Ventilateur Xiaomi Smart Desktop Air Circulation Fan

Le ventilateur Xiaomi Smart Desktop Air Circulation Fan P70 peut désormais être piloté dans Home Assistant grâce à une intégration personnalisée. Voici comment l’ajouter et exploiter toutes ses fonctions.
Ventilateur Xiaomi Smart Desktop Air Circulation Fan

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.

⚠️
Le contenu de cet article a été testé et validé avec la version 2026.7 de Home Assistant.

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émentCaractéristique
Nom du produitXiaomi Smart Desktop Air Circulation Fan
Modèle EuropeBPLDS10DM
TypeVentilateur / circulateur d’air de bureau connecté
Tension nominale12 V ⎓
Puissance nominale18 W
Dimensions324 × 200 × 334 mm
Poids net2,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éeJusqu’à 10 m
Vitesse d’air maximale annoncée5 m/s
Oscillation horizontale120°
Oscillation verticale100°
Réglages de vitesse100 niveaux via Xiaomi Home
ConnectivitéWi-Fi IEEE 802.11 b/g/n 2,4 GHz
AlimentationAdaptateur secteur 18 W fourni
Port d’alimentationUSB-C / Type-C
Longueur du câble1,8 m
Contenu de la boîteVentilateur, 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 :

Releases · PiotrMachowski/Xiaomi-cloud-tokens-extractor
This tool retrieves tokens for all devices connected to Xiaomi cloud and encryption keys for BLE devices. - PiotrMachowski/Xiaomi-cloud-tokens-extractor

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 :

GitHub - syssi/xiaomi_fan: Xiaomi Mi Smart Fan integration for Home Assistant
Xiaomi Mi Smart Fan integration for Home Assistant - syssi/xiaomi_fan

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.

💬
disclaimer
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.