Reverse Engineering - Aldes B200-FAN_T.Flow® Hygro+

Bonjour a tous,
Pour information, je suis équipé d’une VMC double flux Aldes InspirAIR.
Le protocole de communication est bien du ModBus.
Je controle toutes les fonctions grace a un convertisseur USB rs485 connecté sur HA.
Je me permets de suivre cette discussion car toutes les infos sont bonnes a prendre. Et si je peux aider…
Pour le moment, je récupère toutes les infos en ModBus dans un YAML et carte personnelle en JS pour l’interaction. Tout en local, pas de cloud.

3 « J'aime »

Salut @DavidC et merci pour ton message !

Tu utilises Modbus - Home Assistant, c’est bien ça ?

Comment as-tu interprété la data modbus et en particulier tout ce qui est envoi de commande ? Autant lire des états ne me fait pas peur, autant demander des changements d’états sans savoir ce que je fais, je ne suis pas chaud.

Salut, toutes les addresses etaient fournies dans la documentation d’installation.
Pour une VMC, rien de mechant, j’ai commencé directement avec le Pc et un dongle RS485 pour confirmer le comportement de la VMC. Au pire, je remettais la valeure d’origine.
Certains registres sont en lecture seule donc pas de risque en tentant une modification, juste une erreur modbus. Pour les autres, ce sont des valeurs correspondantes a des reglages directement accessibles sur la VMC.
Pour les reglages T1,T2,… Nb Salle de Bain, Nb WC, cellier, tout est retranscrit par un calcule interne a la VMC et change les valeures de pression, vitesse pour correspondre au type de logement.(je pourrais retrouver le tableau des correspondance)
Puis seulement apres j’ai intégré dans home assistant. Avec le meme dongle, creation des sensors modbus en yaml, et modification des valeures avec le service modbus.writeregister.

2 « J'aime »

Bonjours à tous,

Je possède une Dee Fly Cube 300 et j’ai commencé à tester le port iBus et le protocole Modbus. J’ai un Wemos Mini D1 connecté à un RS485 TTL en vue de l’intégrer à HA.

Après plusieurs mails sans réponses chez Aldes est-ce que quelqu’un aurait les codes Modbus pour ma VMC ? J’ai bien trouvé ceux de l’Inspi’Air mais je ne parviens pas à interroger la DF avec.

Si j’arrive à faire fonctionner le tout sous ESPHome je posterai les infos ici.

Merci.

Salut,

Tu n’as pas trouvé ces infos dans le post plus haut ?

Hello, j’ai bien potassé tout ça mais je n’arrive a rien sur le Modbus, je ne connais pas le slave ID de la Dee Fly Cube, j’imagine que ce doit être le 2 car ça semble commun chez Aldes mais ca ne me ressort rien. Faut-il un GND connecté en plus du A et B pour le RS485 ? Sur la VMC il n’y a que A et B dispo.

Au dessus @PaC parle de l’adresse 4.

Bonjour @Alliante ,
Comment as-tu connecté ta VMC Dee Fly Cube 300 à ton convertisseur TTL/RS485 stp?
Je possède la même VMC et je n’ai pas trouvé de connection directe en RS485. :face_with_diagonal_mouth:

Le Ibus est un +24V découpé quand on veut transmettre de l’information.
Voir les mesures de @Neuvidor au début du sujet.
Il faut donc une interface maison Ibus ↔ TTL pour se connecter à la DeeFly Cube avec un ESP.
J’ai prévu de faire un essai cette semaine de vacances pour vérifier mon montage.
(je me suis inspiré de ce qu’il y a dans l’AldesConnectBox.)

Bonjour @DavidC ,
Super ton montage! :+1:
C’est exactement ce que je souhaite faire avec ma DeeFlyCube.
STP, pourrais-tu partager ta documentation sur les registres utilisés?
Eventuellement ta config modbus en yaml?
Par avance merci. :pray:

Salut,

Pour le Wemos j’utilise ceci (Tx et Rx connectés sur le Tx et Rx du Wemos) et pour mes tests avec mon PC ceci.

Je me branche sur le bornier bus de la carte électronique (bornier vert en haut à droite sur cette photo). Une fois connecté la led sur la carte s’allume d’ailleurs.

Pour le moment j’ai bien une réponse en Slave n°4 mais je n’ai pas encore poussé plus loin mes recherches faute de temps.

Pour info je me connecte avec un baud rate de 9600, data bits de 8, stop bits de 1 et la parity sur None mais je ne sais pas si c’est réglages sont corrects n’ayant aucune doc sur le modbus de la Dee Fly Cube.

Salut PaC
Pas de soucis.
Je suis un peux pris pour le moment, mais pas de soucis.
Je vois ca dans la semaine.
Tu as ce qu’il faut pour ta connexion modbus?

Bonjour
Pour commencer, mon « configuration.yaml » pour splitter les fichiers (créée un dossier packages):

############################################
## Organisation des fichiers par packages ##
############################################
homeassistant:
  packages: !include_dir_merge_named packages/
  

Ensuite, dans le dossier packages, j’organise mes differents yaml :
packages/aldes/aldes.yaml =>

aldes_modbus:

  modbus:
    - name: vmc_modbus
      #id: vmc_modbus
      type: serial
      baudrate: 115200
      bytesize: 8
      method: rtu
      parity: E
      # port: /dev/ttyUSB0
      port: /dev/serial/by-id/usb-FTDI_FT232R_USB_UART_A603D7VH-if00-port0
      stopbits: 1
      # delay: 5
      # timeout: 5
      # retries: 5
      # retry_on_empty: true
      sensors:

# # Identification:
  #_____________________________________________________________________________________
  #| ADDR | SIZE | NOM DATA                   | REMARQUES                         | L/E |
  #|______|______|____________________________|___________________________________|_____|
  #|    1 |    2 | Code ID Machine            | Code SAP                          |  r  |
  #|    3 |    4 | Numéro de série            |                                   |  r  |
  #|   12 |    1 | Version Logiciel           |                                   |  r  |
  #|   20 |    1 | Ordre Sauvegarde Parmètres | Mémorisation temporisée Réglages: | r-w |
  #|      |      |                            | 1s: $5678 / 5s: $1234 / 15s: $7890|     |
  #|______|______|____________________________|___________________________________|_____|

  # word swap
  # AABB CCDD > CCDD AABB
  # byte swap
  # AABB CCDD > BBAA DDCC
  # word byte swap
  # AABB CCDD > DDCC BBAA
#
        - unique_id: vmc_identification_code_id_machine
          name: "VMC Code ID Machine"
          slave: 2
          scan_interval: 3600
          data_type: int32
          address: 1 
          count: 2

        - unique_id: vmc_identification_serial
          name: "VMC Numéro de série"
          slave: 2
          scan_interval: 3600
          data_type: int64
          address: 3 
          count: 4

        - unique_id: vmc_identification_version_logiciel
          name: "VMC Version Logiciel"
          slave: 2
          scan_interval: 3600
          data_type: int16
          address: 12
          
        - unique_id: vmc_identification_ordre_sauvegarde_parametres
          name: "VMC Mémorisation Temporisée Réglages"
          slave: 2
          scan_interval: 3600
          data_type: int16
          address: 20
          
          

# # Mode de fonctionnement:
  #_________________________________________________________________________________________
  #| ADDR | SIZE | NOM DATA                   | REMARQUES                             | L/E |
  #|______|______|____________________________|_______________________________________|_____|
  #|  256 |    1 | Mode de Régulation         | 0:Auto 1:Hydro 2:VitesseConstante     | r-w |
  #|  257 |    1 | Vitesse Ventilation        | 0:Vacances 1:Normal 2:Boost 3:Invités | r-w |
  #|  258 |    1 | not used                   |                                       |     |
  #|  259 |    1 | Gestion Bypass Echangeur   | 0:Off 1:Auto 2:Optimisation Hiver     | r-w |
  #|      |      |                            | 3:Optimisation Eté 4:Ouvert           |     |
  #|______|______|____________________________|_______________________________________|_____|
#
        - name: "VMC Mode de fonctionnement"
          unique_id: vmc_mode_de_fonctionnement
          # input_type: holding
          data_type: custom
          slave: 2
          address: 256
          count: 4
          scan_interval: 2
          structure: ">4h"

# # Tempo de fonctionnement:
  #_________________________________________________________________________________________
  #| ADDR | SIZE | NOM DATA                   | REMARQUES                             | L/E |
  #|______|______|____________________________|_______________________________________|_____|
  #|  264 |    1 | Durée Vacances             | 0:infini  nb*5min  Max:113 jours 32544| r-w |
  #|  265 |    1 | Durée pointe Cuisine       | 0:infini  nb*5min  Max:113 jours      | r-w |
  #|  266 |    1 | Durée Boost                | 0:infini  nb*5min  Max:113 jours      | r-w |
  #|  267 |    1 | Durée de vie du filtreur   | en mois   max: 255                    | r-w |
  #|______|______|____________________________|_______________________________________|_____|
#
        - name: "VMC Tempo de fonctionnement"
          unique_id: vmc_tempo_de_fonctionnement
          # input_type: holding
          data_type: custom
          slave: 2
          address: 264
          count: 4
          scan_interval: 5
          structure: ">4h"

# # Configuration de fonctionnement:
  #___________________________________________________________________________________________
  #| ADDR | SIZE | NOM DATA                     | REMARQUES                             | L/E |
  #|______|______|______________________________|_______________________________________|_____|
  #|  272 |    1 | Consigne Débit Extraction    |                                       | r-w |
  #|  273 |    1 | Consigne Débit Insuflation   |                                       |  r  |
  #|  274 |    1 | Consigne Pression Extraction |                                       | r-w |
  #|  275 |    1 | Consigne Pression Insuflation|                                       |  r  |
  #|  276 |    1 | Consigne Vitesse Extraction  |                                       | r-w |
  #|  277 |    1 | Consigne Vitesse Insuflation |                                       |  r  |
  #|  278 |    1 | Balance Débit                |                                       | r-w |
  #|      |      | Insuflation/Extraction       |                                       |     |
  #|______|______|______________________________|_______________________________________|_____|
  #|  282 |    1 | Consigne Temp. Confort Eté   |                                       | r-w |
  #|  284 |    1 | Paramètre 0-10V U1           |                                       | r-w |
  #|  285 |    1 | Paramètre 0-10V U2           |                                       | r-w |
  #|______|______|______________________________|_______________________________________|_____|
#
        - name: "VMC Configuration de fonctionnement"
          unique_id: vmc_configuration_de_fonctionnement
          # input_type: holding
          data_type: custom
          slave: 2
          address: 272
          count: 7
          scan_interval: 10
          structure: ">7h"

        - unique_id: vmc_consigne_temp_confort_ete
          name: "VMC Consigne Temp. Confort Eté "
          slave: 2
          scan_interval: 10
          data_type: int16
          address: 282

        - name: "VMC Paramètre 0-10V"
          unique_id: vmc_0_10v
          # input_type: holding
          data_type: custom
          slave: 2
          address: 284
          count: 2
          scan_interval: 3600
          structure: ">2h"
        
# # Principales Entrées:
  #____________________________________________________________________________________________
  #| ADDR | SIZE | NOM DATA                     | REMARQUES                              | L/E |
  #|______|______|______________________________|________________________________________|_____|
  #|  337 |    1 | Tension Alimentation         |                                        |  r  |
  #|  338 |    1 | Entrée 0-10V                 |                                        |  r  |
  #|  339 |    1 | Etats Switchs                | b0 = SW1 ... b7 = SW8                  |  r  |
  #|  344 |    1 | HIM Installateur             | IHM > b0:Clav.Eco à Membrane b1:Coeur  |  r  |
  #|  346 |    1 | % Usage Filtre               | %                                      |  r  |
  #|  347 |    1 | Temps depuis Reset           | Heures                                 |  r  |
  #|  348 |    1 | Etat ByPass                  | 0:Fermé 1:Ouvert 2:en fermeture        |  r  |
  #|      |      |                              | 3:en ouverture 4:en court-circuit      |     |
  #|      |      |                              | 5:en circuit ouvert 6:sous alimentation|     |
  #|  349 |    1 | Courant consommé par ByPass  | 0 ou >0                                |  r  |
  #|  350 |    1 | Température Air Neuf         | Sonde Température Ext. x0.01°C         |  r  |
  #|  351 |    1 | Température Air Intérieur    | Sonde Température Int. x0.01°C         |  r  |
  #|______|______|______________________________|________________________________________|_____|
  #|  384 |    1 | Code Erreur Actuel           |                                        |  r  |
  #|______|______|______________________________|_ ______________________________________|_____|
#

        - name: "VMC Principales Entrées"
          unique_id: vmc_principales_entrees
          # input_type: holding
          data_type: custom
          slave: 2
          address: 337
          count: 15
          scan_interval: 10
          structure: ">15h"

        - name: "VMC Code Erreur Actuel"
          unique_id: vmc_code_erreur_actuel
          data_type: int16
          slave: 2
          address: 384
          scan_interval: 30

  timer:
    vmc_aldes_duree_timer:
      name: "VMC Timer"
      duration: 9763200
      restore: true

  #   vmc_aldes_duree_cuisine:
  #     name: "VMC Timer Cuisine"
  #     duration: 9763200          
  #     restore: true
      
  #   vmc_aldes_duree_boost:
  #     name: "VMC Timer Boost"
  #     duration: 9763200          
  #     restore: true

  # input_number:
  #   timer_end:
  #     name: TimerDateEnd
  #     min: 0
  #     max: 9999999999999999999999999999999999999999

  sensor:    
    - platform: template
      sensors:
        vmc_aldes:
          unique_id: vmc_aldes
          friendly_name: "VMC Aldes"
          value_template: >
            {% set val = states('sensor.vmc_mode_de_fonctionnement').split(',')[1] %}
            {% if val == "0" %} Vacances {% endif %}
            {% if val == "1" %} Normal {% endif %}
            {% if val == "2" %} Boost {% endif %}
            {% if val == "3" %} Invités {% endif %}
          attribute_templates:
            id_machine: "{{ states('sensor.vmc_code_id_machine') }}"
            serial: "{{ states('sensor.vmc_numero_de_serie') }}"
            version_logiciel: "{{ states('sensor.vmc_version_logiciel') }}"
            ordre_sauvegarde_parametres: "{{ states('sensor.vmc_memorisation_temporisee_reglages') }}"
            mode_de_fonctionnement: "{{ states('sensor.vmc_mode_de_fonctionnement') }}"
            tempo_de_fonctionnement: "{{ states('sensor.vmc_tempo_de_fonctionnement') }}"
            configuration_de_fonctionnement: "{{ states('sensor.vmc_configuration_de_fonctionnement') }}"
            consigne_temp_confort_ete: "{{ states('sensor.vmc_consigne_temp_confort_ete') }}"
            vmc_0_10v: "{{ states('sensor.vmc_parametre_0_10v') }}"
            principales_entrees: "{{ states('sensor.vmc_principales_entrees') }}"
            code_erreur_actuel: "{{ states('sensor.vmc_code_erreur_actuel') }}"

Je regroupe plusieurs infos sur une seul lecture pour éviter de surcharger les appels Modbus. Je divise les infos reçues par le suite dans le code de la carte js.

Dans le dossier www/community je crée un fichier js (kgd-VMC_Aldes-card.js) que j’intègre dans HA

code de la carte:

import { LitElement, html, css, unsafeCSS } from "https://unpkg.com/lit-element@2.5.1/lit-element.js?module";

class KGD_AldesCard extends HTMLElement {
    
    //Selectionnable dans lovelace
    static getConfigElement() {
        return document.createElement("KGD-Aldes-Card-Editor");
    };

    static getStubConfig() {
        return {
            entity: "sensor.vmc_aldes",
            timer: "timer.vmc_aldes_duree_timers"
        };
    };

    set hass(hass) {

        let _this = this;
        const _config = this.config;
        const _state = hass.states[_config.entity];
        let _title = _config.title ? _config.title : "VMC Aldes";
        const _timer = _config.timer;

        const _serial = _state.attributes.serial ? _state.attributes.serial : "N/A";
        let _vitesse_ventilation = _state.attributes.mode_de_fonctionnement ? _state.attributes.mode_de_fonctionnement.toString().split(",")[1] : "N/A";
        let _mode_regulation = _state.attributes.mode_de_fonctionnement ? _state.attributes.mode_de_fonctionnement.toString().split(',')[0] : "N/A";
        let _gestion_bypass = _state.attributes.mode_de_fonctionnement ? _state.attributes.mode_de_fonctionnement.toString().split(",")[3] : "N/A";
        let _duree_vacances = _state.attributes.tempo_de_fonctionnement ? parseInt(_state.attributes.tempo_de_fonctionnement.toString().split(',')[0], 10) * 5 : "N/A";
        let _duree_cuisine = _state.attributes.tempo_de_fonctionnement ? parseInt(_state.attributes.tempo_de_fonctionnement.toString().split(',')[1], 10) * 5 : "N/A";
        let _duree_boost = _state.attributes.tempo_de_fonctionnement ? parseInt(_state.attributes.tempo_de_fonctionnement.toString().split(',')[2], 10) * 5 : "N/A";
        let _duree_vie_filtre = _state.attributes.tempo_de_fonctionnement ? _state.attributes.tempo_de_fonctionnement.toString().split(',')[3] : "N/A";
        let _pourcentage_filtre = _state.attributes.principales_entrees ? _state.attributes.principales_entrees.toString().split(',')[9] : "N/A";
        
        if (!this.vitesseVentilation) {
            this.innerHTML = `
                <ha-card id="${_serial}">

                    <!-- 
                        CARTE LOVELACE
                    -->

                    <h1 id="KGD-CardHeader">
                        <div class="name"> ${_title} </div>
                        <div class="header-icon">
                            <!-- ha-icon name="info" icon="mdi:information-variant-circle-outline"></ha-icon -->
                            <ha-icon name="param" icon="mdi:cog"></ha-icon> 
                        </div>
                    </h1>
                    <div id="KGD-CardvitesseVentilation">
                        <input id="vacances" type="radio" name="vitesseVentilation" value='0' />
                        <input id="normal" type="radio" name="vitesseVentilation" value='1' checked />
                        <input id="boost" type="radio" name="vitesseVentilation" value='2' />
                        <input id="invites" type="radio" name="vitesseVentilation" value='3' />
                        <ha-icon id="AlarmeFiltre" icon="mdi:air-filter"></ha-icon>
                        <div class="circle">                            
                            <div id="a" class="rotate">
                                <label for="vacances">
                                    <ha-icon icon="mdi:beach"></ha-icon>
                                </label>
                            </div>
                            <div id="b" class="rotate">
                                <label for="normal">
                                    <ha-icon icon="mdi:sofa-single"></ha-icon>
                                </label>
                            </div>
                            <div id="c" class="rotate">
                                <label for="program">
                                    <ha-icon icon="mdi:clock-outline"></ha-icon>
                                </label>
                            </div>
                            <div id="d" class="rotate">
                                <label for="boost">
                                    <ha-icon icon="mdi:chef-hat"></ha-icon>
                                </label>
                            </div>
                            <div id="e" class="rotate">
                                <label for="invites">
                                    <ha-icon icon="mdi:account-group"></ha-icon>
                                </label>
                            </div>
                            <div class="mode-de-fonctionnement">
                                <h1>Mode</br>${_state.state}</h1>
                                <div role="spinner"></div>
                                <div class="timer">
                                    <hui-timer-entity-row>
                                        <hui-generic-entity-row> 
                                            <div class="text-content">
                                                
                                            </div> 
                                        </hui-generic-entity-row>
                                    </hui-timer-entity-row>
                                </div>
                            </div>
                        </div>                        
                    </div>

                    <!--
                        CARTE DIALOG PARAMETRES 
                    -->

                    <ha-dialog id="KGD-DialogParametres" hideactions flexcontent>
                        <h1> 
                            <span>${_title}</span>
                            <ha-icon name="fermer" icon="mdi:close"></ha-icon>                            
                        </h1>  
                        <div class="content">
                            <div id="KGD-ModeDeFonctionnement" class="ModFonctionnement">
                                <div id="KGD-ModeRegulation" class='KGD-Border'>
                                    <sablier><sable></sable></sablier>
                                    <input id="MRAuto" type="radio" name="MR" value='0' />
                                    <input id="MRHydro" type="radio" name="MR" value='1' checked />
                                    <input id="MRVitesse" type="radio" name="MR" value='2' />                                                 
                                    <div role="dropdown" class="dropdown">
                                        <h2>MODE DE</br>REGULATION</h2>
                                        <button><ha-icon></ha-icon><span class="arrow-down"></span></button>
                                        <div role="dropdown-content" class="dropdown-content" tabindex="-1">
                                            <label for="MRAuto"><ha-icon icon='mdi:fan-auto'></ha-icon>Auto</label>
                                            <label for="MRHydro"><ha-icon icon='mdi:water-opacity'></ha-icon>Hydro</label>
                                            <label for="MRVitesse"><ha-icon icon='mdi:speedometer'></ha-icon>Vitesse</label>
                                        </div>                                
                                    </div>
                                </div>
                                
                                <div id="KGD-GestionBypass" class='KGD-Border'>
                                    <sablier><sable></sable></sablier>
                                    <input id="GBOff" type="radio" name="GB" value='0' />
                                    <input id="GBAuto" type="radio" name="GB" value="1" checked />
                                    <input id="GBHiver" type="radio" name="GB" value="2" />                                                 
                                    <input id="GBEte" type="radio" name="GB" value="3" />
                                    <input id="GBOuvert" type="radio" name="GB" value="4" />                                                 
                                    <div role="dropdown" class="dropdown">
                                        <h2>GESTION</br>BYPASS</h2>
                                        <button><ha-icon></ha-icon><span class="arrow-down"></span></button>
                                        <div role="dropdown-content" class="dropdown-content" tabindex="-1">
                                            <label for="GBOff"><ha-icon icon='mdi:valve-closed'></ha-icon>Off</label>
                                            <label for="GBAuto"><ha-icon icon='mdi:sun-snowflake-variant'></ha-icon>Auto</label>
                                            <label for="GBHiver"><ha-icon icon='mdi:snowflake'></ha-icon>Hivers</label>
                                            <label for="GBEte"><ha-icon icon='mdi:weather-sunny'></ha-icon>Eté</label>
                                            <label for="GBOuvert"><ha-icon icon='mdi:valve-open'></ha-icon>Ouvert</label>
                                        </div>                                
                                    </div>
                                </div>
                            </div>    

                            <div id="DureeVacances" class='KGD-Border KGD-DureeFonctionnement'>
                                <h2>DUREE DU MODE VACANCES</h2>
                                <sablier><sable></sable></sablier>
                                <div>
                                    <div role="button" class="KGD-Hidden" action="restore">
                                        <ha-icon icon="mdi:restore"></ha-icon>
                                    </div>
                                    <div class="KGD-Selecteur">
                                        <span>J</span>
                                        <button target="days" value='+'><span class="arrow-up"></span></button>
                                        <input disabled type="number" name="days" min=0 max=112 maxlength=3 size=3 step=1 value="000" onfocus="this.select()"/>
                                        <button target="days" value='-'><span class="arrow-down"></span></button>
                                    </div>  
                                    <div class="KGD-Selecteur">
                                        <span>H</span>
                                        <button target="hours" value='+'><span class="arrow-up"></span></button>
                                        <input disabled type="number" name="hours" min=0 max=23 maxlength=2 size=2 step=1 value="00" onfocus="this.select()"/>
                                        <button target="hours" value='-'><span class="arrow-down"></span></button>
                                    </div> 
                                    <div class="KGD-Selecteur">
                                        <span>M</span>
                                        <button target="minutes" value='+'><span class="arrow-up"></span></button>
                                        <input disabled type="number" name="minutes" min=0 max=55 maxlength=2 size="2" step=5 value="00" onfocus="this.select()"/>
                                        <button target="minutes" value='-'><span class="arrow-down"></span></button>
                                    </div>
                                    <div role="button" class="KGD-Hidden" action="valide">
                                        <ha-icon icon="mdi:check"></ha-icon>
                                    </div>                          
                                </div>                            
                            </div>
                            <div id="DureeCuisine" class='KGD-Border KGD-DureeFonctionnement'>
                                <h2>DUREE DU MODE CUISINE</h2>
                                <sablier><sable></sable></sablier>
                                <div>
                                    <div role="button" class="KGD-Hidden" action="restore">
                                        <ha-icon icon="mdi:restore"></ha-icon>
                                    </div>
                                    <div class="KGD-Selecteur">
                                        <span>J</span>
                                        <button target="days" value='+'><span class="arrow-up"></span></button>
                                        <input disabled type="number" name="days" min=0 max=112 maxlength=3 size=3 step=1 value="000" onfocus="this.select()"/>
                                        <button target="days" value='-'><span class="arrow-down"></span></button>
                                    </div>                          
                                    <div class="KGD-Selecteur">
                                        <span>H</span>
                                        <button target="hours" value='+'><span class="arrow-up"></span></button>
                                        <input disabled type="number" name="hours" min=0 max=23 maxlength=2 size=2 step=1 value="00" onfocus="this.select()"/>
                                        <button target="hours" value='-'><span class="arrow-down"></span></button>
                                    </div>                          
                                    <div class="KGD-Selecteur">
                                        <span>M</span>
                                        <button target="minutes" value='+'><span class="arrow-up"></span></button>
                                        <input disabled type="number" name="minutes" min=0 max=55 maxlength=2 size="2" step=5 value="00" onfocus="this.select()"/>
                                        <button target="minutes" value='-'><span class="arrow-down"></span></button>
                                    </div>
                                    <div role="button" class="KGD-Hidden" action="valide">
                                        <ha-icon icon="mdi:check"></ha-icon>
                                    </div>                          
                                </div>                            
                            </div>
                            <div id="DureeBoost" class='KGD-Border KGD-DureeFonctionnement'>
                                <h2>DUREE DU MODE BOOST</h2>
                                <sablier><sable></sable></sablier>
                                <div>
                                    <div role="button" class="KGD-Hidden" action="restore">
                                        <ha-icon icon="mdi:restore"></ha-icon>
                                    </div>
                                    <div class="KGD-Selecteur">
                                        <span>J</span>
                                        <button target="days" value='+'><span class="arrow-up"></span></button>
                                        <input disabled type="number" name="days" min=0 max=112 maxlength=3 size=3 step=1 value="000" onfocus="this.select()"/>
                                        <button target="days" value='-'><span class="arrow-down"></span></button>
                                    </div>                          
                                    <div class="KGD-Selecteur">
                                        <span>H</span>
                                        <button target="hours" value='+'><span class="arrow-up"></span></button>
                                        <input disabled type="number" name="hours" min=0 max=23 maxlength=2 size=2 step=1 value="00" onfocus="this.select()"/>
                                        <button target="hours" value='-'><span class="arrow-down"></span></button>
                                    </div>                          
                                    <div class="KGD-Selecteur">
                                        <span>M</span>
                                        <button target="minutes" value='+'><span class="arrow-up"></span></button>
                                        <input disabled type="number" name="minutes" min=0 max=55 maxlength=2 size="2" step=5 value="00" onfocus="this.select()"/>
                                        <button target="minutes" value='-'><span class="arrow-down"></span></button>
                                    </div>
                                    <div role="button" class="KGD-Hidden" action="valide">
                                        <ha-icon icon="mdi:check"></ha-icon>
                                    </div>                          
                                </div>                            
                            </div>
                            <div id="DureeFiltre" class='KGD-Border KGD-DureeFonctionnement'>
                                <h2>DUREE DU FILTRE</h2>
                                <sablier><sable></sable></sablier>
                                <div>
                                    <div role="button" class="KGD-Hidden" action="restore">
                                        <ha-icon icon="mdi:restore"></ha-icon>
                                    </div>
                                    <div class="KGD-Selecteur">
                                        <span>Mois</span>
                                        <button target="months" value='+'><span class="arrow-up"></span></button>
                                        <input disabled type="number" name="months" min=0 max=24 maxlength=2 size=2 step=1 value="00" onfocus="this.select()"/>
                                        <button target="months" value='-'><span class="arrow-down"></span></button>
                                    </div>
                                    <div id="UtilisationFiltre" role="gauge">
                                        <span>Utilisation</span>
                                        <KGD-Gauge style="  --gauge-color: red;
                                                            --gauge-background: lightgray; 
                                                            --value: 90deg;">
                                            <svg viewBox="-50 -50 100 50" class="gauge">
                                                <path class="dial" d="M -40 0 A 40 40 0 0 1 40 0"></path>
                                                <path class="value" d="M -40 0 A 40 40 0 1 0 40 0" style="transform: rotate(var(--value));"></path>  
                                            </svg>
                                            <span class="strtext">
                                                <span class="text">3</span><span> %</span>
                                            </span>
                                        </KGD-Gauge>
                                    </div>
                                    <div role="button" class="KGD-Hidden" action="valide">
                                        <ha-icon icon="mdi:check"></ha-icon>
                                    </div>                                                              
                                </div>     
                            </div>
                        </div>
                    </ha-dialog>
                </ha-card>
            `;
            _this.header = this.querySelector('#KGD-CardHeader');
            _this.vitesseVentilation = this.querySelector('#KGD-CardvitesseVentilation');
            _this.vitesseVentilation.value = _this.vitesseVentilation.querySelector("input[name='vitesseVentilation']:checked").value;
            _this.alarmeFiltre = _this.vitesseVentilation.querySelector('#AlarmeFiltre');
            _this.spinner = _this.vitesseVentilation.querySelector('div[role="spinner"]');
            _this.dialogParametres = this.querySelector('#KGD-DialogParametres');
            _this.modeFonctionnement = this.querySelector("#KGD-ModeDeFonctionnement");
            _this.modeRegulation = this.querySelector('#KGD-ModeRegulation');
            _this.modeRegulation.registre = 256;
            _this.modeRegulation.value = _this.modeRegulation.querySelector("input[name='MR']:checked").value;
            _this.gestionByPass = this.querySelector('#KGD-GestionBypass');
            _this.gestionByPass.registre = 259;
            _this.gestionByPass.value = _this.gestionByPass.querySelector("input[name='GB']:checked").value;
            _this.dureeVacances = this.querySelector('#DureeVacances');
            _this.dureeVacances.registre = 264;
            _this.dureeVacances.days = _this.dureeVacances.querySelector("input[name='days']");  
            _this.dureeVacances.hours = _this.dureeVacances.querySelector("input[name='hours']");  
            _this.dureeVacances.minutes = _this.dureeVacances.querySelector("input[name='minutes']"); 
            _this.dureeCuisine = this.querySelector('#DureeCuisine');
            _this.dureeCuisine.registre = 265;
            _this.dureeCuisine.days = _this.dureeCuisine.querySelector("input[name='days']");  
            _this.dureeCuisine.hours = _this.dureeCuisine.querySelector("input[name='hours']");  
            _this.dureeCuisine.minutes = _this.dureeCuisine.querySelector("input[name='minutes']");  
            _this.dureeBoost = this.querySelector('#DureeBoost');
            _this.dureeBoost.registre = 266;
            _this.dureeBoost.days = _this.dureeBoost.querySelector("input[name='days']");  
            _this.dureeBoost.hours = _this.dureeBoost.querySelector("input[name='hours']");  
            _this.dureeBoost.minutes = _this.dureeBoost.querySelector("input[name='minutes']");  
            _this.dureeFiltre = this.querySelector('#DureeFiltre');
            _this.dureeFiltre.registre = 267;
            _this.dureeFiltre.months = _this.dureeFiltre.querySelector("input[name='months']");
            _this.utilisationFiltre = this.querySelector("#UtilisationFiltre");  
            _this.utilisationFiltre.pourcent = _this.utilisationFiltre.querySelector(".text");
            _this.utilisationFiltre.gauge = _this.utilisationFiltre.querySelector("KGD-Gauge").style;
            
            const cssstyle = document.createElement('style');
            cssstyle.setAttribute("type","text/css");
            cssstyle.textContent = this.getStyles(_serial);
            this.appendChild(cssstyle);  

        };

        // BOUTON VITESSE VENTILATION
        this.vitesseVentilation.querySelectorAll('input[name="vitesseVentilation"]').forEach( e => {
            e.onclick = function () {
                if (e.value !== 'P' && e.value !== _vitesse_ventilation) {
                    if (hass.states[_timer] && hass.states[_timer].state !== "idle") {
                       hass.callService('timer', 'cancel', { entity_id: _timer });
                       
                    };
                    if (typeof _this.interval !== 'undefined' && _this.interval !== null) {
                        clearInterval(_this.interval);
                        _this.interval = null;
                    };
                    _this.vitesseVentilation.value = _this.vitesseVentilation.querySelector("input[name='vitesseVentilation']:checked").value;            
                    _this.vitesseVentilation.changed = true;
                    hass.callService('modbus', 'write_register', { hub: 'vmc_modbus', slave: 2, address: 257, value: [e.value] });
                    _this.vitesseVentilation.querySelector('.mode-de-fonctionnement h1').innerHTML = `Mode</br>${_state.state}`;
                    _this.spinner.classList.remove('KGD-Hidden');
                    
                    //start  timer
                    if (parseInt(_this.vitesseVentilation.value) !== 1) {
                        let _duree = _this.vitesseVentilation.value == 0 ? _duree_vacances * 60 :
                                     _this.vitesseVentilation.value == 2 ? _duree_cuisine * 60:
                                                                           _duree_boost * 60;
                        hass.callService('timer', 'start', { entity_id: _timer, duration: _duree });
                    };
                };
            };
        });

        // BOUTON PARAMETRE => Affiche DialogParametres
        this.header.querySelectorAll('ha-icon[name="param"]').forEach( element => {
            element.style.cursor = 'pointer';
            element.onclick = function () {
                _this.dialogParametres.setAttribute('open', '');
            }
        });

        // BOUTON FERMER PARAMETRE
        this.dialogParametres.querySelectorAll('ha-icon[name="fermer"]').forEach( element => {
            element.style.cursor = 'pointer';
            element.onclick = function () {
                _this.dialogParametres.removeAttribute('open');
            }
        });

        // BOUTON MODE REGULATION && BYPASS
        this.modeFonctionnement.querySelectorAll('input').forEach( ipt => {
            ipt.style.display = "none";
            ipt.onclick = function() {
                if (ipt.name == 'MR') {
                    if (ipt.value !== _mode_regulation) {
                        if (_this.modeRegulation.querySelector("sablier").style.display == "none") {
                            _this.modeRegulation.changed = true;
                            _this.modeRegulation.value = _this.modeRegulation.querySelector("input:checked").value;            
                            hass.callService('modbus', 'write_register', { hub: 'vmc_modbus', slave: 2, address: _this.modeRegulation.registre, value: [ipt.value] });
                            _this.modeRegulation.querySelector('sablier').style.display = "block";
                            let _val = ['Auto','Hydro','Vitesse'];
                            let _ico = ['mdi:fan-auto','mdi:water-opacity','mdi:speedometer'];
                            _this.modeRegulation.querySelector('button').innerHTML = `<ha-icon icon='${_ico[ipt.value]}'></ha-icon>${_val[ipt.value]}<span class="arrow-down"></span>`;
                        };                    
                    };
                };
                if (ipt.name == 'GB') {
                    if (ipt.value !== _gestion_bypass) {
                        if (_this.gestionByPass.querySelector("sablier").style.display == "none") {
                            _this.gestionByPass.changed = true;
                            _this.gestionByPass.value = _this.gestionByPass.querySelector("input:checked").value;            
                            hass.callService('modbus', 'write_register', { hub: 'vmc_modbus', slave: 2, address: _this.gestionByPass.registre, value: [ipt.value] });
                            _this.gestionByPass.querySelector('sablier').style.display = "block";
                            let _val = ['Off','Auto','Hivers','Eté','Ouvert'];
                            let _ico = ['mdi:valve-closed','mdi:sun-snowflake-variant','mdi:snowflake','mdi:weather-sunny','mdi:valve-open'];
                            _this.gestionByPass.querySelector('button').innerHTML = `<ha-icon icon='${_ico[ipt.value]}'></ha-icon>${_val[ipt.value]}<span class="arrow-down"></span>`;
                        };                    
                    };
                };
            };
        });        
        this.modeFonctionnement.querySelectorAll('div[role="dropdown"]').forEach( mF => {
            mF.addEventListener('focusin', ()=>{
                mF.querySelector('div[role="dropdown-content"]').style.display = 'block';
                mF.querySelector('div[role="dropdown-content"]').focus();
                mF.querySelectorAll('div[role="dropdown-content"]').forEach( dropdown => {
                    dropdown.addEventListener('focusout', function(){
                        dropdown.style.display = 'none';            
                    });
                    dropdown.querySelectorAll('label').forEach( label =>{
                        label.onclick = function(){
                            dropdown.style.display = "none";
                        };
                    });
                });
            });

        });

        // DUREE DE FONCTIONNEMENT
        this.dialogParametres.querySelectorAll('.KGD-DureeFonctionnement').forEach(div => {
            div.querySelectorAll('button').forEach(e => {
                e.onclick = function() {                    
                    let _t = div.getAttribute('id') == 'DureeVacances' ? 
                                [_this.dureeVacances, _duree_vacances] : 
                                div.getAttribute('id') == 'DureeCuisine' ?
                                    [_this.dureeCuisine, _duree_cuisine] :
                                    div.getAttribute('id') == 'DureeBoost' ?
                                        [_this.dureeBoost, _duree_boost] :
                                        [_this.dureeFiltre, _duree_vie_filtre];
                    if (_t[0].querySelector("sablier").style.display == "none") {
                        _t[0].changed = true;
                        _t[0].querySelectorAll('div[role="button"]').forEach(bt => {
                            bt.style.cursor = 'pointer';
                            bt.classList.remove("KGD-Hidden");
                            bt.onclick = function() {
                                if(bt.getAttribute("action") == "restore") { 
                                    try {_t[0].days.value = Math.floor(_t[1] * 60 / 86400);} catch {};
                                    try {_t[0].hours.value = String(Math.floor((_t[1] * 60 % 86400) / 3600)).padStart( 2, '0')} catch {};
                                    try {_t[0].minutes.value = String(Math.floor((_t[1] * 60 % 3600) / 60)).padStart( 2, '0')} catch {};
                                    try {_t[0].months.value = parseInt(_t[1])} catch {};
                                    _t[0].querySelectorAll('div[role="button"]').forEach(d => {d.classList.add("KGD-Hidden");});
                                    _t[0].changed = false;
                                };
                                if(bt.getAttribute("action") == "valide") { 
                                    _t[0].querySelectorAll('div[role="button"]').forEach(d => {d.classList.add("KGD-Hidden");});
                                    _t[0].querySelector("sablier").style.display = 'block';
                                    hass.callService('modbus', 'write_register', { hub: 'vmc_modbus', slave: 2, address: _t[0].registre, value: [_this.getDuree(_t[0])] });
                                    
                                };
                            };
                        });                                       
                        switch (e.getAttribute('target')) {
                            case "days":
                                _t[0].days.value = e.value == '+' ?
                                    Math.min(Math.max(_t[0].days.min, Math.round((parseInt(_t[0].days.value) + parseInt(_t[0].days.step)) / _t[0].days.step) * _t[0].days.step), _t[0].days.max):
                                    Math.min(Math.max(_t[0].days.min, Math.round((parseInt(_t[0].days.value) - parseInt(_t[0].days.step)) / _t[0].days.step) * _t[0].days.step), _t[0].days.max);
                                break;
                            case "hours":
                                _t[0].hours.value = e.value == '+' ?
                                    String(Math.min(Math.max(_t[0].hours.min, Math.round((parseInt(_t[0].hours.value) + parseInt(_t[0].hours.step)) / _t[0].hours.step) * _t[0].hours.step), _t[0].hours.max)).padStart( 2, '0'):
                                    String(Math.min(Math.max(_t[0].hours.min, Math.round((parseInt(_t[0].hours.value) - parseInt(_t[0].hours.step)) / _t[0].hours.step) * _t[0].hours.step), _t[0].hours.max)).padStart( 2, '0');
                                break;
                            case "minutes":
                                _t[0].minutes.value = e.value == '+' ?
                                    String(Math.min(Math.max(_t[0].minutes.min, Math.round((parseInt(_t[0].minutes.value) + parseInt(_t[0].minutes.step)) / _t[0].minutes.step) * _t[0].minutes.step), _t[0].minutes.max)).padStart( 2, '0'):
                                    String(Math.min(Math.max(_t[0].minutes.min, Math.round((parseInt(_t[0].minutes.value) - parseInt(_t[0].minutes.step)) / _t[0].minutes.step) * _t[0].minutes.step), _t[0].minutes.max)).padStart( 2, '0');
                                break;
                            case "months":
                                _t[0].months.value = e.value == '+' ?
                                    Math.min(Math.max(_t[0].months.min, Math.round((parseInt(_t[0].months.value) + parseInt(_t[0].months.step)) / _t[0].months.step) * _t[0].months.step), _t[0].months.max):
                                    Math.min(Math.max(_t[0].months.min, Math.round((parseInt(_t[0].months.value) - parseInt(_t[0].months.step)) / _t[0].months.step) * _t[0].months.step), _t[0].months.max);
                                break;
                            
                            default:
                                break;
                 
                            };
                    };
                };
            });
        });
                
        // MAJ
        ///////
            
        if(!this.isUpdating) {

            try {
        
                // MODE REGULATION
                let _val, _ico;
                _val = ['Auto','Hydro','Vitesse'];
                _ico = ['mdi:fan-auto','mdi:water-opacity','mdi:speedometer'];
                if (_this.modeRegulation.changed === true) {
                    if (_this.modeRegulation.value == _mode_regulation) {
                        _this.modeRegulation.changed = false;
                        _this.modeRegulation.querySelector("sablier").style.display = "none";
                        _this.modeRegulation.querySelector('button').innerHTML = `<ha-icon icon='${_ico[_mode_regulation]}'></ha-icon>${_val[_mode_regulation]}<span class="arrow-down"></span>`;
                    }
                } else {
                    _this.modeRegulation.value = _mode_regulation;
                    _this.modeRegulation.querySelector("sablier").style.display = "none";
                    _this.modeRegulation.querySelector('button').innerHTML = `<ha-icon icon='${_ico[_mode_regulation]}'></ha-icon>${_val[_mode_regulation]}<span class="arrow-down"></span>`;
                };
                
                // GESTION BYPASS
                _val = ['Off','Auto','Hivers','Eté','Ouvert'];
                _ico = ['mdi:valve-closed','mdi:sun-snowflake-variant','mdi:snowflake','mdi:weather-sunny','mdi:valve-open'];
                if (_this.gestionByPass.changed === true) {
                    if (_this.gestionByPass.value == _gestion_bypass) {
                        _this.gestionByPass.changed = false;
                        _this.gestionByPass.querySelector("sablier").style.display = "none";
                        _this.gestionByPass.querySelector('button').innerHTML = `<ha-icon icon='${_ico[_gestion_bypass]}'></ha-icon>${_val[_gestion_bypass]}<span class="arrow-down"></span>`;
                    }
                } else {
                    _this.gestionByPass.value = _gestion_bypass;
                    _this.gestionByPass.querySelector("sablier").style.display = "none";
                    _this.gestionByPass.querySelector('button').innerHTML = `<ha-icon icon='${_ico[_gestion_bypass]}'></ha-icon>${_val[_gestion_bypass]}<span class="arrow-down"></span>`;
                };
                
                // DUREE VACANCES
                if (_this.dureeVacances.changed === true) {
                    if (_this.dureeVacances.days.value == Math.floor(_duree_vacances * 60 / 86400) &&
                        _this.dureeVacances.hours.value == String(Math.floor((_duree_vacances * 60 % 86400) / 3600)).padStart( 2, '0') &&
                        _this.dureeVacances.minutes.value == String(Math.floor((_duree_vacances * 60 % 3600) / 60)).padStart( 2, '0'))
                        {
                        _this.dureeVacances.changed = false;
                        _this.dureeVacances.querySelectorAll('div[role="button"]').forEach(d => {d.classList.add("KGD-Hidden");});
                        _this.dureeVacances.querySelector("sablier").style.display = "none";
                    }
                } else {
                        _this.dureeVacances.days.value = Math.floor(_duree_vacances * 60 / 86400);
                        _this.dureeVacances.hours.value = String(Math.floor((_duree_vacances * 60 % 86400) / 3600)).padStart( 2, '0');
                        _this.dureeVacances.minutes.value = String(Math.floor((_duree_vacances * 60 % 3600) / 60)).padStart( 2, '0');
                        _this.dureeVacances.querySelector("sablier").style.display = "none";
                };

                // DUREE CUISINE
                if (_this.dureeCuisine.changed === true) {
                    if (_this.dureeCuisine.days.value == Math.floor(_duree_cuisine * 60 / 86400) &&
                        _this.dureeCuisine.hours.value == String(Math.floor((_duree_cuisine * 60 % 86400) / 3600)).padStart( 2, '0') &&
                        _this.dureeCuisine.minutes.value == String(Math.floor((_duree_cuisine * 60 % 3600) / 60)).padStart( 2, '0'))
                        {
                        _this.dureeCuisine.changed = false;
                        _this.dureeCuisine.querySelectorAll('div[role="button"]').forEach(d => {d.classList.add("KGD-Hidden");});
                        _this.dureeCuisine.querySelector("sablier").style.display = "none";
                    }
                } else {
                        _this.dureeCuisine.days.value = Math.floor(_duree_cuisine * 60 / 86400);
                        _this.dureeCuisine.hours.value = String(Math.floor((_duree_cuisine * 60 % 86400) / 3600)).padStart( 2, '0');
                        _this.dureeCuisine.minutes.value = String(Math.floor((_duree_cuisine * 60 % 3600) / 60)).padStart( 2, '0');
                        _this.dureeCuisine.querySelector("sablier").style.display = "none";
                };

                // DUREE BOOST
                if (_this.dureeBoost.changed === true) {
                    if (_this.dureeBoost.days.value == Math.floor(_duree_boost * 60 / 86400) &&
                        _this.dureeBoost.hours.value == String(Math.floor((_duree_boost * 60 % 86400) / 3600)).padStart( 2, '0') &&
                        _this.dureeBoost.minutes.value == String(Math.floor((_duree_boost * 60 % 3600) / 60)).padStart( 2, '0'))
                        {
                        _this.dureeBoost.changed = false;
                        _this.dureeBoost.querySelectorAll('div[role="button"]').forEach(d => {d.classList.add("KGD-Hidden");});
                        _this.dureeBoost.querySelector("sablier").style.display = "none";
                    }
                } else {
                        _this.dureeBoost.days.value = Math.floor(_duree_boost * 60 / 86400);
                        _this.dureeBoost.hours.value = String(Math.floor((_duree_boost * 60 % 86400) / 3600)).padStart( 2, '0');
                        _this.dureeBoost.minutes.value = String(Math.floor((_duree_boost * 60 % 3600) / 60)).padStart( 2, '0');
                        _this.dureeBoost.querySelector("sablier").style.display = "none";
                };

                // DUREE FILTRE
                if (_this.dureeFiltre.changed === true) {
                    if (parseInt(_this.dureeFiltre.months.value) == parseInt(_duree_vie_filtre)) {
                        _this.dureeFiltre.changed = false;
                        _this.dureeFiltre.querySelectorAll('div[role="button"]').forEach(d => {d.classList.add("KGD-Hidden");});
                        _this.dureeFiltre.querySelector("sablier").style.display = "none";
                    }
                } else {
                        _this.dureeFiltre.months.value = _duree_vie_filtre;
                        _this.dureeFiltre.querySelector("sablier").style.display = "none";
                };

                // GAUGE FILTRE
                _this.utilisationFiltre.pourcent.innerHTML = parseInt(_pourcentage_filtre);
                let _deg = parseInt(_pourcentage_filtre)*1.8 + 'deg';
                _this.utilisationFiltre.gauge.setProperty('--value', _deg);
                let _color = parseInt(_pourcentage_filtre) > 90 ? 'red' : parseInt(_pourcentage_filtre) > 75 ? 'orange' : parseInt(_pourcentage_filtre) > 50 ? 'yellow' : 'green';
                _this.utilisationFiltre.gauge.setProperty('--gauge-color', _color);
                _this.alarmeFiltre.style.display = parseInt(_pourcentage_filtre) >= 90 ? 'block' : 'none';

                // VITESSE VENTILATION
                if (_this.vitesseVentilation.changed === true) {
                    if (_this.vitesseVentilation.value == _vitesse_ventilation) {
                        _this.vitesseVentilation.changed = false;
                        _this.vitesseVentilation.querySelector('.mode-de-fonctionnement h1').innerHTML = `Mode</br>${_state.state}`;
                        _this.spinner.classList.add('KGD-Hidden');

                        //creation affichage timer
                        if (parseInt(_this.vitesseVentilation.value) == 1) {
                            _this.vitesseVentilation.querySelector('.mode-de-fonctionnement .timer').innerHTML = null;
                            if (typeof _this.interval !== 'undefined' && _this.interval !== null) {
                                clearInterval(_this.interval);
                                _this.interval = null;
                            }
                        } else {
                            if (typeof _this.interval == 'undefined' || _this.interval == null) {
                                _this.interval = setInterval( function() {
                                    let _millis = Date.parse(hass.states[_timer].attributes.finishes_at) - Date.now();
                                    _this.vitesseVentilation.querySelector('.mode-de-fonctionnement .timer').innerHTML = _this.convertDHMS(_millis);
                                }, 500);
                            };
                        };
                    };
                } else {
                    if (_this.vitesseVentilation.querySelector(`input[name="vitesseVentilation"]:checked`).value !== _vitesse_ventilation) {
                        if (typeof _this.interval !== 'undefined' && _this.interval !== null) {
                            _this.vitesseVentilation.querySelector('.mode-de-fonctionnement .timer').innerHTML = null;
                            clearInterval(_this.interval);
                            _this.interval = null;
                        };
                    };

                    _this.vitesseVentilation.querySelectorAll(`input[name="vitesseVentilation"]`)[_vitesse_ventilation].checked = true;
                    _this.vitesseVentilation.value = _this.vitesseVentilation.querySelector(`input[name="vitesseVentilation"]:checked`).value;
                    _this.spinner.classList.add('KGD-Hidden');
                    _this.vitesseVentilation.querySelector('.mode-de-fonctionnement h1').innerHTML = `Mode</br>${_state.state}`;

                    //creation affichage timer
                    if (parseInt(_this.vitesseVentilation.value) == 1) {
                        _this.vitesseVentilation.querySelector('.mode-de-fonctionnement .timer').innerHTML = null;
                        if (typeof _this.interval !== 'undefined' && _this.interval !== null) {
                            clearInterval(_this.interval);
                            _this.interval = null;
                        }
                    } else {
                        if (typeof _this.interval == 'undefined' || _this.interval == null) {
                            _this.interval = setInterval( function() {
                                let _millis = Date.parse(hass.states[_timer].attributes.finishes_at) - Date.now();
                                _this.vitesseVentilation.querySelector('.mode-de-fonctionnement .timer').innerHTML = _this.convertDHMS(_millis);
                            }, 500);
                        };
                    };
                };
            } catch (err) {};
        };
    };
  
    setConfig(config) {
      this.config = config;
      this.isUpdating = false;
    };
  
    getCardSize() {
      return 2;
    };

    getDuree(target) {
        let D, H, M, V;
        //Add = target == _this.dureeVacances ? 264 : target == _this.dureeCuisine ? 265 : target == _this.dureeBoost ? 266 : 267;
        if (target.registre === 264 || target.registre === 265 || target.registre === 266) {
            D = target.days.value * 24 * 60;
            H = target.hours.value * 60;
            M = target.minutes.value;
            V = (parseInt(D) + parseInt(H) + parseInt(M)) / 5;
        }
        if (target.registre === 267) {
            V = parseInt(target.months.value);
        }
        return V;
    };

    convertDHMS(milliseconds, style) {

        const totalSeconds = Math.floor(milliseconds / 1000);
        let days = Math.floor(totalSeconds / 86400);
        const hours = Math.floor((totalSeconds % 86400) / 3600);
        const minutes = Math.floor((totalSeconds % 3600) / 60);
        const seconds = totalSeconds % 60;
        
        if (style == 'number') {
            days = `${String(days).padStart(3, '0')}:`;
        }else{
            if (days == 0) { days = ''; }
            if (days == 1) { days = `${days} jour - ` }
            if (days > 1) { days = `${days} jours - ` }
        }
        return `${days}${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`;
    };

    getStyles(_serial) {
        const _background_color = '#80B4DE88';
        const ha = `ha-card[id='${_serial}']`;
        let _style = `

                ${ha} button {
                    cursor: pointer;   
                }

                ${ha} #KGD-ModeDeFonctionnement {
                    display: flex;
                    min-width: 325px;
                }

                ${ha} div[role="dropdown"] button {
                    --mdc-icon-size 32px;
                    width: -webkit-fill-available;
                    height: auto;
                    border-radius: 8px;
                    padding: 4px;
                    padding-right: 8px;
                    margin: 12px 8px;
                    cursor: pointer;
                    display: flex;
                    align-items: center;
                    font-size: 14px;
                    font-weight: bold;
                    justify-content: space-between;
                    background-color: ${_background_color};
                    border: 2px outset ${_background_color};                    
                }
                
                ${ha} div[role="dropdown-content"] {
                    display: none;
                    position: absolute;
                    margin: 0;
                    padding: 0;
                    background-color: var(--secondary-background-color);                
                    width: 38%;
                    max-height: 248px;
                    overflow: auto;
                    box-shadow: 0px 8px 16px 0px rgba(0, 0, 0, 0.2);
                    z-index: 1;
                    border-radius: 5px;
                    border-color: var(--primary-text-color)                   
                }

                ${ha} div[role="dropdown-content"] label {
                    padding: 10px 20px;
                    cursor: pointer;
                    display: flex;
                    align-items: center;
                    font-size: 14px;
                    font-weight: bold;
                    justify-content: space-between;
                }

                ${ha} div[role="gauge"] {
                    display: flex;
                    flex-direction: column;
                    align-items: stretch;
                    margin: 2px 8px;
                }

                ${ha} div[role="gauge"] KGD-Gauge {
                    width: 100%;
                    max-width: 250px;
                    display: block;
                    height: -webkit-fill-available;
                    padding: 8px 0 0 0;
                }

                ${ha} div[role="gauge"] .value {
                    fill: none;
                    stroke-width: 15;
                    stroke: var(--gauge-color);
                    transition: all 1s ease 0s;
                }

                ${ha} div[role="gauge"] .dial {
                    fill: none;
                    stroke: var(--gauge-background);
                    stroke-width: 15;
                }

                ${ha} div[role="gauge"] .strtext {
                    top: -10px;
                    position: relative;
                }

                ${ha} .KGD-Hidden {
                    display: none;
                }
                ${ha} sablier {
                    z-index: 1;
                    position: relative;
                    top: 65px;
                    margin: -20px;
                    width: fit-content;
                    box-sizing: border-box;
                    border-top: 7px solid; 
                    border-bottom: 7px solid; 
                    border-left: 0; 
                    border-right: 0; 
                    border-color: var(--accent-color) transparent var(--accent-color) transparent;
                    animation: sablier-anim 2.5s linear infinite;
                }

                ${ha} sablier sable {
                    display: block;
                    height: 0;
                    width: 0;
                    box-sizing: border-box;
                    -moz-box-sizing: border-box;
                    -webkit-box-sizing: border-box;
                    border: 14px solid; 
                    border-color: var(--accent-color) transparent var(--accent-color) transparent;
                }

                ${ha} div[role="spinner"] {
                    width: 24px;
                    height: 24px;
                    border: 4px solid;
                    border-color: #3d5af1 transparent #3d5af1 transparent;
                    border-radius: 50%;
                    animation: spin-anim 1.2s linear infinite;
                }
                
                @keyframes spin-anim {
                    0% {
                        transform: rotate(0deg);
                    }
                    100% {
                        transform: rotate(360deg);
                    }
                }
                @keyframes sablier-anim {
                    0% {
                        transform: rotate(0deg);
                    }
                    25% {
                        transform: rotate(180deg);
                    }
                    50% {
                        transform: rotate(180deg);
                    }
                    75% {
                        transform: rotate(360deg);
                    }                    
                    100% {
                        transform: rotate(360deg);
                    }
                }

                ${ha} #KGD-CardHeader {
                    background-color: ${_background_color};
                    margin: 0;
                    padding: 16px 12px 12px 12px;
                    border-radius: 8px 8px 0 0;
                    display: flex;
                    justify-content: space-between;
                }

                ${ha} input[name="vitesseVentilation"] {
                    display: none;
                }

                ${ha} .circle {
                    height: 250px;
                    width: 250px;
                    display: flex;
                    justify-content: center;
                    align-items: center;
                    margin: 36px auto;
                    position: relative;
                }

                ${ha} .mode-de-fonctionnement{
                    text-align: center;
                    line-height: 2em;
                    margin-top: -16px;
                    display: flex;
                    flex-direction: column;
                    align-items: center;
                }

                ${ha} #AlarmeFiltre {
                    right: 24px;
                    float: right;
                    color: red;
                    position: absolute;
                    -webkit-animation: fadeinout 3s infinite forwards;
                    animation: fadeinout 3s infinite forwards;
                    opacity: 0;
                }

                @-webkit-keyframes fadeinout {
                    75% { opacity: 1; }
                }
                
                @keyframes fadeinout {
                    75% { opacity: 1; }
                }

                ${ha} .rotate {
                    height: 149px;
                    position: absolute;
                    top: 125px;
                    left: 125px;
                    width: 56px;
                    margin-left: -28px;
                    display: flex;
                    align-items: flex-end;
                }

                ${ha} .rotate ha-icon {
                    border-radius: 50%;
                    border: solid 1px ${_background_color};
                    height: 56px;
                    width: 56px;
                    display: flex;
                    align-items: center;
                    justify-content: center;
                    background: ${_background_color};
                }
                
                ${ha} #a, ${ha} #a ha-icon ,
                ${ha} #b , ${ha} #b ha-icon , 
                ${ha} #c, ${ha} #c ha-icon, 
                ${ha} #d, ${ha} #d ha-icon, 
                ${ha} #e, ${ha} #e ha-icon {
                    transition:1s;
                }

                ${ha} #a,
                ${ha} #b, 
                ${ha} #c, 
                ${ha} #d, 
                ${ha} #e {
                    transform-origin: 28px 0;
                }

                ${ha} #a{
                    z-index:1;
                }

                ${ha} label{
                    cursor:pointer
                }

                /* vacances */                
                ${ha} #vacances:checked ~ .circle #a {transform: rotate(+180deg);}
                ${ha} #vacances:checked ~ .circle #a ha-icon {transform: rotate(-180deg);}
                ${ha} #vacances:checked ~ .circle #b {transform: rotate(-45deg);}
                ${ha} #vacances:checked ~ .circle #b ha-icon {transform: rotate(+45deg);}
                ${ha} #vacances:checked ~ .circle #c {transform: rotate(-15deg);}
                ${ha} #vacances:checked ~ .circle #c ha-icon {transform: rotate(+15deg);}
                ${ha} #vacances:checked ~ .circle #d {transform: rotate(+15deg);}
                ${ha} #vacances:checked ~ .circle #d ha-icon {transform: rotate(-15deg);}
                ${ha} #vacances:checked ~ .circle #e {transform: rotate(+45deg);}
                ${ha} #vacances:checked ~ .circle #e ha-icon {transform: rotate(-45deg);}
                /* normal */
                ${ha} #normal:checked ~ .circle #b {transform: rotate(-180deg);}
                ${ha} #normal:checked ~ .circle #b ha-icon {transform: rotate(+180deg);}
                ${ha} #normal:checked ~ .circle #c {transform: rotate(-45deg);}
                ${ha} #normal:checked ~ .circle #c ha-icon {transform: rotate(+45deg);}
                ${ha} #normal:checked ~ .circle #d {transform: rotate(-15deg);}
                ${ha} #normal:checked ~ .circle #d ha-icon {transform: rotate(+15deg);}
                ${ha} #normal:checked ~ .circle #e {transform: rotate(+15deg);}
                ${ha} #normal:checked ~ .circle #e ha-icon {transform: rotate(-15deg);}
                ${ha} #normal:checked ~ .circle #a {transform: rotate(+45deg);}
                ${ha} #normal:checked ~ .circle #a ha-icon {transform: rotate(-45deg);}
                /* program */
                ${ha} #program:checked ~ .circle #c {transform: rotate(-180deg);}
                ${ha} #program:checked ~ .circle #c ha-icon {transform: rotate(+180deg);}
                ${ha} #program:checked ~ .circle #d {transform: rotate(-45deg);}
                ${ha} #program:checked ~ .circle #d ha-icon {transform: rotate(+45deg);}
                ${ha} #program:checked ~ .circle #e {transform: rotate(-15deg);}
                ${ha} #program:checked ~ .circle #e ha-icon {transform: rotate(+15deg);}
                ${ha} #program:checked ~ .circle #a {transform: rotate(+15deg);}
                ${ha} #program:checked ~ .circle #a ha-icon {transform: rotate(-15deg);}
                ${ha} #program:checked ~ .circle #b {transform: rotate(-315deg);}
                ${ha} #program:checked ~ .circle #b ha-icon {transform: rotate(+315deg);}
                /* boost */
                ${ha} #boost:checked ~ .circle #d {transform: rotate(-180deg);}
                ${ha} #boost:checked ~ .circle #d ha-icon {transform: rotate(+180deg);}
                ${ha} #boost:checked ~ .circle #e {transform: rotate(-45deg);}
                ${ha} #boost:checked ~ .circle #e ha-icon {transform: rotate(+45deg);}
                ${ha} #boost:checked ~ .circle #a {transform: rotate(-15deg);}
                ${ha} #boost:checked ~ .circle #a ha-icon {transform: rotate(+15deg);}
                ${ha} #boost:checked ~ .circle #b {transform: rotate(-345deg);}
                ${ha} #boost:checked ~ .circle #b ha-icon {transform: rotate(+345deg);}
                ${ha} #boost:checked ~ .circle #c {transform: rotate(-315deg);}
                ${ha} #boost:checked ~ .circle #c ha-icon {transform: rotate(+315deg);}
                /* invites */
                ${ha} #invites:checked ~ .circle #e {transform: rotate(-180deg);}
                ${ha} #invites:checked ~ .circle #e ha-icon {transform: rotate(+180deg);}
                ${ha} #invites:checked ~ .circle #a {transform: rotate(-45deg);}
                ${ha} #invites:checked ~ .circle #a ha-icon {transform: rotate(+45deg);}                
                ${ha} #invites:checked ~ .circle #b {transform: rotate(-375deg);}
                ${ha} #invites:checked ~ .circle #b ha-icon {transform: rotate(+375deg);}                
                ${ha} #invites:checked ~ .circle #c {transform: rotate(-345deg);}
                ${ha} #invites:checked ~ .circle #c ha-icon {transform: rotate(+345deg);}                
                ${ha} #invites:checked ~ .circle #d {transform: rotate(-315deg);}
                ${ha} #invites:checked ~ .circle #d ha-icon {transform: rotate(+315deg);}

                ${ha} ha-dialog h1 {
                    background-color: ${_background_color};
                    margin: -24px -24px 0 -24px;
                    padding: 16px 12px 12px 12px;
                    border-radius: 8px 8px 0 0;
                    display: flex;
                    justify-content: space-between;
                }

                ${ha} #KGD-DialogParametres {
                    overflow: hidden;
                }

                ${ha} #KGD-DialogParametres .content {
                    overflow-y: auto;
                    overflow-x: hidden;
                    margin-top: 12px;
                }

                ${ha} .KGD-Border {
                    border-radius: 16px;
                    border: 2px inset ${_background_color};
                    padding: 0;
                    text-align: -webkit-center;
                    width: -webkit-fill-available;
                    margin: 0 4px;
                }
                
                ${ha} h2 {
                    background-color: ${_background_color};
                    padding : 4px;
                    margin: 0px;
                    border-radius: 14px 14px 0 0;
                    width: -webkit-fill-available;
                    font-size: 14px;
                }

                
                ${ha} .KGD-DureeFonctionnement {
                    margin: 4px;
                }
                
                ${ha} .KGD-DureeFonctionnement>div{
                    display: flex;
                    justify-content: center;
                    padding: 8px;
                }
                
                ${ha} .KGD-DureeFonctionnement>div div[role="button"][action="restore"],
                ${ha} .KGD-DureeFonctionnement>div div[role="button"][action="valide"] {
                    margin: auto;
                    background-color: #F00A;
                    cursor: pointer;
                    --mdc-icon-size: 28px;
                    padding: 8px;
                    border-radius: 40%;
                }

                ${ha} .KGD-DureeFonctionnement>div div[role="button"][action="valide"] {
                    background-color: #0F0A;
                }

                ${ha} .KGD-Selecteur {
                    display: flex;
                    flex-direction: column;
                    align-items: stretch;
                    margin: 2px;
                }
                
                ${ha} button {
                    background-color: ${_background_color};
                    border: 2px outset ${_background_color};
                }

                ${ha} .arrow-down {
                    width: 0;
                    height: 0;
                    display: inline-block;
                    vertical-align: middle;
                    border-style: solid;
                    border-width: 8px 8px 0 8px;
                    border-color: var(--primary-text-color) transparent transparent transparent;
                }

                ${ha} .arrow-up {
                    width: 0;
                    height: 0;
                    display: inline-block;
                    vertical-align: middle;
                    border-style: solid;
                    border-width: 0px 8px 8px 8px;
                    border-color: transparent transparent var(--primary-text-color) transparent;
                }

                ${ha} .KGD-Selecteur input {
                    user-select: none;
                    font-size: large;
                    text-align: center;
                    -moz-appearance: textfield;
                    appearance: textfield;
                    width: 38px;
                    padding: 2px 0 0 0;
                }
                
                ${ha} .KGD-Selecteur input::-webkit-inner-spin-button,
                ${ha} .KGD-Selecteur input::-webkit-outer-spin-button { 
                    -webkit-appearance: none; 
                    margin: 0; 
                }


                  
                }

        `;
        return css`${unsafeCSS(_style)}`;
    };

};
  
customElements.define('KGD-Aldes-Card', KGD_AldesCard);
  
////////////////////////////////////////////////////

class KGD_AldesCardEditor extends LitElement {



};

customElements.define("KGD-Aldes-Card-Editor", KGD_AldesCardEditor);

window.customCards = window.customCards || [];
window.customCards.push({
    type: "kgd-aldes-card",
    name: "KGD Aldes Card",
    preview: true, // Optional - defaults to false
    description: "Carte Aldes VMC", // Optional
    configurable: true, // Optional - defaults to false
});

Gros travaille toujours en cours.
Je dois encore faire la consigne de température été/hivers,…
Toutes vos remarques sont les bienvenu.
Je n’ai pas réussi à trouver comment réinitialiser le filtre, du coup je le fais sur la VMC (comme j’y suis pour le changement…)
Si ce contenu peu aider :wink:

3 « J'aime »

J’oubliai, le code de la carte pour l’intégrer dans le lovelace, mais normalement elle est sélectionnable dans les cartes avec le code minimum:

type: custom:kgd-aldes-card
entity: sensor.vmc_aldes
timer: timer.vmc_aldes_duree_timer

3 « J'aime »

Bonjour @DavidC ,
Merci mille fois pour ton partage de configuration.
C’est une avancée énorme! Beau travail! :+1:

Pour info, j’ai réussi à intégrer ta carte sans soucis grâce à tes explications claires, puis j’ai bricolé une interface maison entre ma vmc DeeFlyCube300 et HA en passant par l’Ibus.
Malheureusement je n’ai pas encore trouvé la bonne configuration.
Il y a bien des échanges entre la VMC et HA mais je n’arrive pas à récupérer les bonnes données. :confounded:
Je continue à investiguer.
Encore merci :pray:

Bonjour @Alliante ,

Si j’ai bien compris, tu connectes une interface USB-RS485 directement sur le connecteur ibus de ta DeeFlyCube.
Dans ce cas, je serais très surpris que ton montage fonctionne car le ibus semble bien utiliser le protocole modbus mais le « format » électrique n’est pas du RS485.
Le ibus est un format spécial composé d’un +24V continu haché par le signal à transmettre.
Si la petite led verte au dessus du connecteur reste constamment allumée c’est qu’il y a un problème car elle doit clignoter au rythme du signal transmis.

En connectant une interface RS485 sur le ibus tu risques d’endommager ton interface car le RS485 ne supporte que rarement plus de 6V.

Dans le cas de @DavidC, il s’agit d’une InspirAir qui possède un connecteur dédié au format électrique RS485.
Il n’existe pas sur la DeeFlyCube300… Il faut donc bricoler, ce que je suis en train de faire.
Désolé, sinon ce serait trop simple! :wink:

Vous n’avez pas de RS485 ???

Ah ok ca explique pourquoi quand j’envoie une requête je reçois « CRC Error - Unknown modbus function code ».

Je m’étais référé à la doc de leur module Modbus qui disait :

Voici les principales spécifications :
• Protocole MODBUS RTU (binaire).
• Couche physique : porte de communication isolée RS485-Asynchrone. Une connexion 3 fils avec GND est donc requise.
• Baudrate d’usine par défaut : 9600. Baudrate possibles : 1200, 4800, 9600 ou 19200.

Du coup la Dee Fly Cube serait la seule à avoir un protocole différent des autres ? Ca serait un peu bizarre non ?

Non du tout, il y a un port 2 pins appelé « Bus » et un port 2 pins « Kitchen » pour connecter un bouton poussoir pour activer le mode Boost si je ne me trompe pas.

Ben non pas de RS485 sur la DeeFlyCube, ou alors je n’ai pas trouvé… :sleepy:

Hello @Alliante,

Le protocole semble bien être du MODBUS RTU mais malheureusement la couche physique n’est pas du RS485… :wink:
Il faut donc un convertisseur ibus ↔ usb maison.
J’ai l’impression que cela existe en option chez Aldes mais cela coûte 1/3 du prix de la VMC! :scream: