Sommaire
Introduction
J’ai profité de l’ajout récent du support du Xiaomi Smart Desktop Air Circulation Fan P70 dans l’intégration personnalisée syssi/xiaomi_fan pour réaliser ce test et l’intégrer à Home Assistant.
En effet, malgré sa sortie il y a déjà plus d’un an, ce ventilateur n’est toujours pas pris en charge par l’intégration native Xiaomi Home de Home Assistant. Il faut donc passer par une intégration personnalisée pour pouvoir l’utiliser correctement, récupérer ses différentes entités et accéder à ses fonctions avancées.
Ce modèle est pourtant particulièrement intéressant pour une installation domotique. Il s’agit d’un ventilateur compact de bureau, capable de faire circuler l’air dans une pièce grâce à son oscillation horizontale et verticale. Il peut être piloté depuis l’application Xiaomi Home, mais aussi intégré à Home Assistant grâce à son adresse IP locale et à son token Xiaomi.
Dans cet article, nous allons voir comment installer le ventilateur dans l’application Xiaomi Home, récupérer son token avec Xiaomi-cloud-tokens-extractor, installer l’intégration syssi/xiaomi_fan depuis HACS, puis créer une carte Home Assistant complète pour contrôler toutes ses fonctions.
Unboxing

La boîte de la version européenne contient officiellement :
- le Xiaomi Smart Desktop Air Circulation Fan
- l’adaptateur secteur de 1,8 m
- le manuel d’utilisation
Le ventilateur est déjà assemblé. Il suffit de retirer les protections, de le poser sur une surface stable et de le brancher. Avec une base de seulement 20 cm de large, il peut facilement trouver sa place sur un bureau, une table de nuit ou une étagère.
La version européenne conserve le design blanc minimaliste, la poignée de transport intégrée et la tête motorisée sur deux axes. Elle ne possède pas de batterie interne, mais peut fonctionner avec une batterie externe compatible délivrant au moins 18 W par USB-C. Xiaomi annonce jusqu’à 26 heures d’utilisation au niveau 1 avec une batterie externe de 10 000 mAh, dans des conditions précises et sans oscillation.
Caractéristiques
Voici les caractéristiques du Xiaomi Smart Desktop Air Circulation Fan version Europe.
| Élément | Caractéristique |
|---|---|
| Nom du produit | Xiaomi Smart Desktop Air Circulation Fan |
| Modèle Europe | BPLDS10DM |
| Type | Ventilateur / circulateur d’air de bureau connecté |
| Tension nominale | 12 V ⎓ |
| Puissance nominale | 18 W |
| Dimensions | 324 × 200 × 334 mm |
| Poids net | 2,0 kg |
| Niveau sonore maximal | ≤ 60 dB(A) |
| Niveau sonore minimal annoncé | 28,4 dB(A) au niveau 1, en mode brise directe sans oscillation |
| Débit d’air maximal annoncé | 1 180 m³/h |
| Portée d’air annoncée | Jusqu’à 10 m |
| Vitesse d’air maximale annoncée | 5 m/s |
| Oscillation horizontale | 120° |
| Oscillation verticale | 100° |
| Réglages de vitesse | 100 niveaux via Xiaomi Home |
| Connectivité | Wi-Fi IEEE 802.11 b/g/n 2,4 GHz |
| Alimentation | Adaptateur secteur 18 W fourni |
| Port d’alimentation | USB-C / Type-C |
| Longueur du câble | 1,8 m |
| Contenu de la boîte | Ventilateur, adaptateur secteur, manuel d’utilisation |
Installation
Avant de pouvoir intégrer le Xiaomi Smart Desktop Air Circulation Fan dans Home Assistant, il faut d’abord l’associer à l’application Xiaomi Home.
Pour cela, branchez le ventilateur, puis ouvrez l’application Xiaomi Home sur votre smartphone. Appuyez sur Ajouter un appareil. L’ajout peut se faire de deux façons : soit par recherche automatique, si le ventilateur est détecté à proximité, soit en scannant le QR code fourni dans le manuel d’utilisation.
Une fois l’appareil sélectionné, suivez les instructions affichées dans l’application pour connecter le ventilateur à votre réseau Wi-Fi. Pensez à utiliser un réseau Wi-Fi 2,4 GHz, car le ventilateur n’est pas compatible avec le Wi-Fi 5 GHz.
Lorsque l’installation est terminée, vérifiez que le ventilateur répond correctement depuis Xiaomi Home. Vous pourrez ensuite passer à l’étape suivante : la récupération du token nécessaire à son intégration dans Home Assistant.
Récupération du token
Maintenant que le ventilateur est correctement installé dans l’application Xiaomi Home, nous allons récupérer son token. Celui-ci sera nécessaire pour permettre à Home Assistant de communiquer localement avec le ventilateur.
Pour cela, nous allons utiliser l’outil Xiaomi-cloud-tokens-extractor, disponible sur GitHub.
Rendez-vous sur la page GitHub du projet, puis téléchargez le fichier .exe correspondant à votre système Windows :
Une fois le fichier téléchargé, lancez l’application.
Au démarrage, l’outil vous demandera de vous connecter avec les identifiants de votre compte Xiaomi Home. Saisissez l’adresse e-mail et le mot de passe utilisés dans l’application Xiaomi Home.
Selon votre compte, une étape de sécurité peut apparaître, comme un captcha ou une validation supplémentaire. Suivez simplement les instructions affichées à l’écran afin de finaliser la connexion.
Une fois connecté, l’outil vous demandera de choisir le serveur Xiaomi sur lequel vos équipements sont installés. Il est important de sélectionner la même région que celle utilisée dans l’application Xiaomi Home.
Par exemple :
de = Europe / Allemagne
fr = France, si disponible selon l’outil
cn = Chine
sg = Singapour
us = États-Unis
Après validation, Xiaomi-cloud-tokens-extractor affiche la liste des appareils associés à votre compte. Recherchez votre ventilateur dans la liste, par exemple :
Name: Xiaomi Smart Desktop Air Circulation Fan
Model: xiaomi.fan.p70
IP address: 192.168.1.50
Token: 1234567890abcdef1234567890abcdef
Notez soigneusement deux informations :
L’adresse IP correspond à l’adresse locale du ventilateur sur votre réseau. Le token est une clé d’accès nécessaire à Home Assistant pour piloter l’appareil.
Il est conseillé de conserver ce token dans un endroit sûr et de ne pas le partager publiquement. Une personne disposant du token et d’un accès à votre réseau local pourrait contrôler le ventilateur.
Une fois le token récupéré, nous pouvons passer à l’installation de l’intégration Home Assistant.
Installation de l’intégration Xiaomi Fan
Pour intégrer le Xiaomi Smart Desktop Air Circulation Fan à Home Assistant, nous allons utiliser l’intégration personnalisée Xiaomi Fan développée par syssi.
Cette intégration est disponible sur GitHub :
Le plus simple est de l’installer directement depuis HACS.
Ouvrez Home Assistant, puis rendez-vous dans HACS. Puis, recherchez :
Xiaomi Mi Smart Pedestal Fan Integration
Sélectionnez l’intégration, puis cliquez sur Télécharger. Une fois l’installation terminée, redémarrez Home Assistant afin que l’intégration soit correctement chargée.
Après le redémarrage, nous pourrons déclarer le ventilateur dans le fichier configuration.yaml à l’aide de l’adresse IP et du token récupérés précédemment.
Pour cela, ouvrez votre fichier configuration.yaml. Vous pouvez le modifier de plusieurs façons :
- avec l’add-on File editor
- avec Samba share
- avec Visual Studio Code
Ajoutez ensuite les lignes suivantes :
fan:
- platform: xiaomi_miio_fan
name: Xiaomi Smart Desktop Air Circulation Fan
host: 192.168.1.50
token: 1234567890abcdef1234567890abcdef
model: xiaomi.fan.p70
Remplacez bien les valeurs suivantes par celles récupérées précédemment avec Xiaomi-cloud-tokens-extractor :
host: 192.168.1.50
token: 1234567890abcdef1234567890abcdef
Le champ host correspond à l’adresse IP locale de votre ventilateur. Le champ token correspond au token Xiaomi de l’appareil.
Le modèle à utiliser pour le Xiaomi Smart Desktop Air Circulation Fan Europe est :
model: xiaomi.fan.p70
Enregistrez ensuite les modifications.
Avant de redémarrer Home Assistant, vérifiez que la configuration est valide. Si aucune erreur n’est détectée, vous pouvez redémarrer Home Assistant.
Après le redémarrage, le ventilateur devrait apparaître dans vos entités Home Assistant avec un nom proche de :
fan.xiaomi_smart_desktop_air_circulation_fan

Vous pourrez ensuite l’ajouter à votre dashboard et l’utiliser dans vos automatisations.
Intégrer le ventilateur dans votre dashboard
Pour rendre l’utilisation du ventilateur plus agréable dans Home Assistant, j’ai créé une carte personnalisée avec button-card. Cette carte permet de regrouper toutes les commandes importantes du Xiaomi Smart Desktop Air Circulation Fan au même endroit, directement depuis le dashboard.
L’objectif est d’éviter de devoir ouvrir plusieurs entités ou services séparément. Depuis cette carte, il est possible de piloter le ventilateur de manière complète et visuelle.
La carte donne accès aux fonctions suivantes :
- activation et désactivation du ventilateur
- Animation de l'image, quand le ventilateur est en marche et mouvement du ventilateur quand l'oscillation (horizontale ou verticale) est activée
- réglage de la vitesse avec un slider
- activation ou désactivation de l’oscillation horizontale
- activation ou désactivation de l’oscillation verticale
- déplacement manuel de la tête du ventilateur vers le haut, le bas, la gauche et la droite
- réglage de l’angle d’oscillation horizontal
- réglage de l’angle d’oscillation vertical
- configuration d’un minuteur pour éteindre automatiquement le ventilateur
- activation ou désactivation du mode Naturel
- activation ou désactivation de la LED
- activation ou désactivation du bip sonore
- activation ou désactivation de la sécurité enfant
Cette carte rend l’intégration beaucoup plus confortable au quotidien. Elle permet de contrôler précisément la direction du flux d’air, l’intensité de ventilation et les options secondaires sans passer par les outils développeur ou les services Home Assistant.
C’est particulièrement pratique dans une pièce de vie ou un bureau, où l’on peut rapidement adapter le comportement du ventilateur selon la température, la position dans la pièce ou le niveau de confort souhaité.
Une fois la carte ajoutée au dashboard, le ventilateur devient aussi simple à utiliser qu’avec l’application Xiaomi Home, tout en conservant les avantages de Home Assistant : automatisations, scènes, contrôle local et intégration avec les autres capteurs de la maison.

Code button-card templates
Avant d’ajouter la carte du ventilateur dans le dashboard, il faut d’abord ajouter les templates Button Card nécessaires à son fonctionnement.
Ces templates doivent être placés tout au début de l'éditeur de configuration, avant la définition des vues et des cartes. Ils permettent de réutiliser une même mise en forme pour plusieurs boutons, afin d’obtenir une carte plus propre, plus lisible et plus simple à modifier par la suite.
Pour accéder à l’éditeur de configuration du dashboard, ouvrez votre dashboard Home Assistant, puis cliquez sur le crayon en haut à droite pour passer en mode édition.
Cliquez ensuite sur les trois points en haut à droite, puis sélectionnez :
Éditeur de configuration
Une fois l’éditeur ouvert, ajoutez la partie dédiée aux templates au tout début du fichier YAML.
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"
L’intérêt est de centraliser la mise en forme des boutons. Si vous souhaitez modifier l’apparence des boutons plus tard, il suffira de modifier le template une seule fois, au lieu de modifier chaque bouton individuellement.
Après avoir ajouté les templates, enregistrez la configuration du dashboard. Vous pourrez ensuite ajouter la carte Button Card complète du Xiaomi Smart Desktop Air Circulation Fan.
Code carte button-card
type: custom:button-card
entity: fan.xiaomi_smart_desktop_air_circulation_fan
name: Xiaomi Smart Desktop Air Circulation Fan
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: 70px;
height: 70px;
left: 27px;
top: 17px;
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: -5px;
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: 7px;
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: 18px;
height: 18px;
margin-top: -5.5px;
border: 3px solid var(--card-background-color, #fff);
border-radius: 50%;
appearance: none;
-webkit-appearance: none;
background: var(--p70-accent);
box-shadow: 0 2px 7px rgba(0, 0, 0, .24);
}
.p70-speed-range::-moz-range-track {
height: 7px;
border-radius: 999px;
background: rgba(127, 127, 127, 0.20);
}
.p70-speed-range::-moz-range-progress {
height: 7px;
border-radius: 999px;
background: var(--p70-accent);
}
.p70-speed-range::-moz-range-thumb {
width: 14px;
height: 14px;
border: 3px solid var(--card-background-color, #fff);
border-radius: 50%;
background: var(--p70-accent);
box-shadow: 0 2px 7px rgba(0, 0, 0, .24);
}
.p70-speed-range:focus-visible::-webkit-slider-thumb {
box-shadow:
0 0 0 4px color-mix(in srgb, var(--p70-accent) 25%, transparent),
0 2px 7px rgba(0, 0, 0, .24);
}
.p70-speed-range:disabled {
cursor: not-allowed;
opacity: .5;
}
styles:
card:
- padding: 8px 8px 8px 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
cycle:
- width: 100%
- "--grid-card-gap": 6px
custom_fields:
power:
card:
type: custom:button-card
entity: fan.xiaomi_smart_desktop_air_circulation_fan
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 === 'Natural' ? '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/xiaomi_fan_p70_product.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="Xiaomi Smart Fan P70" 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>
<span class="p70-chip">V ${angleV}°</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.xiaomi_smart_desktop_air_circulation_fan';
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: 4
square: false
cards:
- type: custom:button-card
template: p70_move_compact
entity: fan.xiaomi_smart_desktop_air_circulation_fan
icon: mdi:chevron-left
variables:
direction: left
- type: custom:button-card
template: p70_move_compact
entity: fan.xiaomi_smart_desktop_air_circulation_fan
icon: mdi:chevron-up
variables:
direction: up
- type: custom:button-card
template: p70_move_compact
entity: fan.xiaomi_smart_desktop_air_circulation_fan
icon: mdi:chevron-down
variables:
direction: down
- type: custom:button-card
template: p70_move_compact
entity: fan.xiaomi_smart_desktop_air_circulation_fan
icon: mdi:chevron-right
variables:
direction: right
modes:
card:
type: grid
columns: 3
square: false
cards:
- type: custom:button-card
template: p70_toggle_compact
entity: fan.xiaomi_smart_desktop_air_circulation_fan
variables:
active: "[[[ return entity.attributes.mode === \"Natural\"; ]]]"
service_on: xiaomi_miio_fan.fan_set_natural_mode_on
service_off: xiaomi_miio_fan.fan_set_natural_mode_off
icon: mdi:leaf
name: Naturel
label: >-
[[[ return entity.attributes.mode === "Natural" ? "Activé" :
"Désactivé"; ]]]
- type: custom:button-card
template: p70_small_compact
entity: fan.xiaomi_smart_desktop_air_circulation_fan
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.xiaomi_smart_desktop_air_circulation_fan
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_toggle_compact
entity: fan.xiaomi_smart_desktop_air_circulation_fan
variables:
active: "[[[ return Boolean(entity.attributes.vertical_oscillate); ]]]"
service_on: xiaomi_miio_fan.fan_set_vertical_oscillation_on
service_off: xiaomi_miio_fan.fan_set_vertical_oscillation_off
icon: mdi:swap-vertical-bold
name: Vertical
label: >-
[[[ return entity.attributes.vertical_oscillate ? "Activée" :
"Arrêtée"; ]]]
- type: custom:button-card
template: p70_small_compact
entity: fan.xiaomi_smart_desktop_air_circulation_fan
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.xiaomi_smart_desktop_air_circulation_fan
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.xiaomi_smart_desktop_air_circulation_fan
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_toggle_compact
entity: fan.xiaomi_smart_desktop_air_circulation_fan
variables:
active: "[[[ return Boolean(entity.attributes.child_lock); ]]]"
service_on: xiaomi_miio_fan.fan_set_child_lock_on
service_off: xiaomi_miio_fan.fan_set_child_lock_off
icon: mdi:lock-outline
name: Verrou
label: >-
[[[ return entity.attributes.child_lock ? "Verrouillé" : "Libre";
]]]
cycle:
card:
type: grid
columns: 3
square: false
cards:
- type: custom:button-card
template: p70_small_compact
entity: fan.xiaomi_smart_desktop_air_circulation_fan
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.xiaomi_smart_desktop_air_circulation_fan
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
- type: custom:button-card
template: p70_small_compact
entity: fan.xiaomi_smart_desktop_air_circulation_fan
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.xiaomi_smart_desktop_air_circulation_fan
data:
angle: |
[[[
const values = [30, 60, 90, 120];
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.xiaomi_smart_desktop_air_circulation_fan
icon: mdi:angle-acute
name: Angle V
label: "[[[ return `${entity.attributes.vertical_angle ?? \"—\"}°`; ]]]"
tap_action:
action: call-service
service: xiaomi_miio_fan.fan_set_vertical_oscillation_angle
target:
entity_id: fan.xiaomi_smart_desktop_air_circulation_fan
data:
vertical_angle: |
[[[
const values = [30, 60, 90, 100];
const current = Number(entity.attributes.vertical_angle ?? 0);
return values.find(v => v > current) ?? values[0];
]]]
haptic: light
Image utilisée sur la carte, à placer dans le dossier /config/www/images/.

Conclusion
Le Xiaomi Smart Desktop Air Circulation Fan est une très bonne surprise. Malgré son format compact, il offre un flux d’air puissant, une oscillation horizontale et verticale efficace, ainsi qu’un fonctionnement discret à faible vitesse. Il trouve facilement sa place sur un bureau, une table de nuit ou dans un salon, tout en restant suffisamment performant pour faire circuler l’air dans une pièce entière.
Son intégration dans l’application Xiaomi Home est simple, et l’ajout dans Home Assistant via l’intégration syssi/xiaomi_fan lui donne une vraie valeur ajoutée. Une fois le token récupéré et la configuration terminée, on obtient un contrôle complet du ventilateur directement depuis son dashboard domotique : vitesse, oscillations, angles, LED, bip sonore, minuteur et déplacements de la tête.
Même si l’absence de support dans l’intégration native Xiaomi Home de Home Assistant est regrettable, l’intégration personnalisée comble très bien ce manque. Elle permet d’exploiter pleinement les possibilités du modèle P70 et de l’intégrer dans des automatisations avancées, par exemple selon la température, l’humidité ou la présence dans la pièce.
Pour finir, le Xiaomi Smart Desktop Air Circulation Fan est un excellent choix pour ceux qui recherchent un ventilateur connecté, compact, silencieux et compatible avec une installation domotique avancée. C’est un produit discret, efficace et très agréable à utiliser au quotidien.
Il existe également une version sur pied, le Xiaomi Smart Standing Air Circulation Fan. Plus imposante et plus performante, elle peut être intéressante si vous cherchez un ventilateur capable de couvrir une plus grande pièce ou si vous préférez un modèle à poser directement au sol plutôt que sur une table ou un bureau.
Bonne nouvelle : cette version est de plus compatible avec l’intégration syssi/xiaomi_fan. Elle peut donc, elle aussi, être intégrée à Home Assistant afin de profiter du contrôle local, des automatisations et d’une carte personnalisée dans votre dashboard.
Ce test n’est pas sponsorisé. Le ventilateur a été acheté et testé en conditions réelles, et ce compte rendu reflète uniquement mon expérience.