Gestion ZEN de sa piscine avec iopool et Home Assistant

Découvrez comment intégrer votre sonde iopool avec Home Assistant pour gérer votre piscine de manière efficace ! Ce tutoriel vous guide pour la mise en place de la sonde, la création d'entités et l'automatisation de la filtration de piscine en utilisant AppDaemon et iopool Pump Manager.
Gestion ZEN de sa piscine avec iopool et Home Assistant

Sommaire

Qui n'a jamais souhaité garder l'eau de sa piscine cristalline ? Ceci n'est pas toujours une chose aisée, mais avec les évolutions technologiques, c'est désormais plus simple et l'on peut profiter de sa piscine l'esprit ZEN !

Qui est iopool ?

iopool est une société belge qui propose une sonde connectée (EcO) sans abonnement pour vous aider à gérer votre piscine ou spa.

Outre la sonde, iopool c’est aussi une application mobile, des produits d’entretien et des accessoires pour piscine.

Une sonde pour faire quoi ?

Cette sonde EcO vous permet de collecter toutes les 15 minutes, différents indicateurs utiles pour garder une eau saine tels que :

  • La température
  • Le pH
  • La capacité de désinfection (orp)
Installation facile
Application & Sonde iopool

La sonde est Bluetooth (vous devez donc avoir votre téléphone à proximité pour collecter les informations à jour), mais iopool vend un relais en pack ou accessoire qui permet de faire la passerelle entre le Bluetooth de la sonde et votre Wi-Fi afin d'envoyer l'ensemble des informations collectées sur les serveurs iopool. C'est donc une solution Cloud.

iopool met à disposition une API dont la documentation est disponible ici.

🎁 Un petit cadeau

Si vous lisez cet article c'est que le sujet vous intéresse et vous n'avez peut-être pas encore franchi le pas de devenir un utilisateur iopool.

Cet article va peut-être vous convaincre.

Voici un code de réduction de 25€ (je bénéficierais aussi de 25€ en bon d'achat en tant que parrain) pour l'achat d'un pack de démarrage.

Récupérer les informations dans Home Assistant

🍿
Une vidéo explicative est disponible à la fin de cet article sur la chaîne Youtube HACF
ℹ️
Nous allons utiliser plusieurs types d'entités que nous allons récupérer via RESTful Sensor.
Pour plus de simplicité, nous allons les déclarer au sein d'un package Home Assistant.

Home Assistant ne peut récupérer que les informations que le cloud iopool met à disposition. Ce qui veut dire que si les données ne sont pas correctes ou à jour sur l'application mobile iopool, les données ne seront pas correctes dans Home Assistant.

Récupérer sa clé API iopool

Pour cela, rendez-vous dans votre application iopool et :

  • Sélectionnez le menu Plus (en bas à droite).
  • Allez dans Paramètres.
  • Récupérez votre clé API.

Déclaration des packages Home Assistant

Ouvrez votre configuration.yaml Home Assistant avec l'éditeur que vous utilisez habituellement.

Il faut ajouter une configuration packages dans la partie homeassistant: telle que ci-dessous :

homeassistant:
  # Load packages
  packages: !include_dir_merge_named includes/packages

Déclaration du répertoire des packages

Si vous avez déjà une référence de ce type dans votre configuration et que vous la comprenez, vous pouvez passer au chapitre suivant (mais gardez bien en mémoire le répertoire associé).

Dans cet exemple, les packages seront des fichiers différents, présents dans le répertoire includes/packages. Si le répertoire n'existe pas, créez le.

Concernant l'utilisation de !include_dir_merge_named, je vous invite à comprendre son utilisation ici.

Déclaration de sa clé API dans secrets.yaml

Ouvrez votre secrets.yaml avec l'éditeur que vous utilisez habituellement.

Ajoutez la référence à votre clé iopool récupérée précédemment :

# iopool
iopool_api_key: icimacléapiiopool

Ajout de la clé API iopool dans secrets.yaml

ℹ️
L'ajout de votre clé API dans secrets.yaml n'est pas obligatoire, mais recommandée afin de pouvoir partager du code YAML sereinement.
Le reste de ce tutoriel considère que vous utilisez secrets.yaml. Dans le cas contraire, il vous faudra adapter un peu la configuration dans le chapitre suivant.

Récupération de votre identifiant de bassin

Il est nécessaire de connaitre son identifiant de bassin afin d'interroger l'API iopool.

Malheureusement, cet identifiant n'est pas visible sur l'interface de l'application mobile, mais peut être récupérée facilement.

Pour cela, il vous suffit de lancer l'une de ces commandes selon votre système d'exploitation :

curl --header 'x-api-key: icimacléapiiopool' https://api.iopool.com/v1/pools/

Commande pour système Linux

$headers=@{}
$headers.Add("x-api-key", "icimacléapiiopool")
$response = Invoke-WebRequest -Uri 'https://api.iopool.com/v1/pools/' -Method GET -Headers $headers

Commande pour système Windows

⚠️
Attention à bien changer icimacléapiiopool par votre clé API réelle récupérée dans les étapes précédentes.

La réponse sera de ce type :

[
  {
    "id": "1aaa22b3-ccc4-4567-d888-e999ff000000",
    "title": "Le nom de mon bassin",
    "latestMeasure": {
      "temperature": 23.129907809491385,
      "ph": 7.422857142857143,
      "orp": 660,
      "mode": "standard",
      "isValid": true,
      "ecoId": "/Keb7cMf",
      "measuredAt": "2024-05-24T14:04:00.000Z"
    },
    "mode": "STANDARD",
    "hasAnActionRequired": false,
    "advice": {
      "filtrationDuration": 4
    }
  }
]

Le contenu du champ id est votre identifiant de bassin. Si vous avez plusieurs sondes, vous aurez plusieurs fois l'exemple ci-dessus, il vous faudra donc choisir le bon identifiant.

Déclaration de votre package iopool

Créer un fichier iopool.yaml dans le dossier includes/packages .

⚠️
Nous partons du principe que vous avez déclaré le dossier pour vos packages tel que défini dans le paragraphe "Déclaration des packages Home Assistant".
Dans le cas contraire, Merci d'ajuster le dossier dans lequel créer le fichier.

Ce fichier va contenir l'ensemble des entités à créer, peu importe leur type.

Commençons par détailler les éléments et vous trouverez, à la fin de ce chapitre, le fichier complet.

iopool:
  sensor:
    - platform: rest
      unique_id: fabc1ee2-0bbe-416e-b23d-2474ac25fe4e
      name: iopool
      resource: https://api.iopool.com/v1/pool/<votre_id_de_bassin_ici>
      value_template: "{{ value_json.title }}"
      json_attributes:
        - id
        - latestMeasure
        - hasAnActionRequired
        - advice
        - mode
      headers:
        x-api-key: !secret iopool_api_key
      scan_interval: 300
      icon: mdi:pool

La première ligne iopool: permet de déclarer le nom du package. Il est nécessaire, car nous utilisons include_dir_merge_named dans la déclaration de notre répertoire de package.

Ensuite, nous déclarons un sensor qui sera alimenté par la plateforme Rest qui permet à Home Assistant de faire des requêtes API.

Le sensor portera le nom sensor.iopool et aura comme état le nom de votre bassin. Ces données seront rafraichies toutes les 5 minutes (300 secondes).

Il contiendra aussi plusieurs attributs qui servent de stockage de données. Je ne rentrerai pas dans les détails, car ces attributs ont peu d'importance à cette étape.

⚠️
Faites bien attention à remplacer votre ID de bassin à la fin de l'URL dans resource:

Maintenant que nous avons créé l'entité en mesure de récupérer les informations sur l'API iopool, il nous faut créer des entités pour stocker séparément chacune des informations afin de plus facilement les exploiter par la suite.

template:
    - sensor:
        - name: "temperature_iopool_pool"
          unique_id: b336b008-dc88-4e3b-afd9-d662979fb0c1$
          state: "{{ state_attr('sensor.iopool', 'latestMeasure')['temperature'] | round(2) }}"
          device_class: temperature
          unit_of_measurement: "°C"
          state_class: measurement
          icon: mdi:pool-thermometer
          attributes:
            source: "{{ state_attr('sensor.iopool', 'latestMeasure')['mode'] }}"
            isValid: "{{ state_attr('sensor.iopool', 'latestMeasure')['isValid'] }}"
            measuredAt: "{{ state_attr('sensor.iopool', 'latestMeasure')['measuredAt'] }}"
            
        - name: "ph_iopool_pool"
          unique_id: f4804a67-1224-4507-a4fb-21d983958b7c
          state: "{{ state_attr('sensor.iopool', 'latestMeasure')['ph'] | round(1) }}"
          unit_of_measurement: "pH"
          attributes:
            source: "{{ state_attr('sensor.iopool', 'latestMeasure')['mode'] }}"
            isValid: "{{ state_attr('sensor.iopool', 'latestMeasure')['isValid'] }}"
            measuredAt: "{{ state_attr('sensor.iopool', 'latestMeasure')['measuredAt'] }}"
            
        - name: "orp_iopool_pool"
          unique_id: e0ef9122-c53a-41ae-be72-517f3fcbb443
          state: "{{ state_attr('sensor.iopool', 'latestMeasure')['orp'] | round(0) }}"
          unit_of_measurement: "mV"
          attributes:
            source: "{{ state_attr('sensor.iopool', 'latestMeasure')['mode'] }}"
            isValid: "{{ state_attr('sensor.iopool', 'latestMeasure')['isValid'] }}"
            measuredAt: "{{ state_attr('sensor.iopool', 'latestMeasure')['measuredAt'] }}"
            
        - name: "recommanded_filtration_iopool_pool"
          unique_id: f53659ba-922f-4861-9198-73a7dd43ae6a
          state: "{{ state_attr('sensor.iopool', 'advice')['filtrationDuration'] * 60 }}"
          device_class: duration
          unit_of_measurement: "min"
          icon: mdi:sun-clock-outline
          
        - name: "mode_iopool_pool"
          unique_id: af6db587-be33-44e7-950c-fa52f0453d1f
          state: "{{ state_attr('sensor.iopool', 'mode') }}"
          icon: mdi:auto-mode

    - binary_sensor:
        - name: "required_actions_iopool_pool"
          unique_id: fb6bb7e0-86ad-4f27-90ee-47c39db0ab12
          state: "{{ state_attr('sensor.iopool', 'hasAnActionRequired') }}"
          device_class: problem
          icon: mdi:checkbox-marked-circle-plus-outline
⚠️
Notre template: doit être au même niveau d'indentation que le sensor que nous avons déclaré précédemment. Dans le doute, n'hésitez pas à vous référer à la version complète du fichier à la fin de ce chapitre.

Comme vous pouvez le voir, nous utilisons un modèle (template) pour aller collecter les informations présente dans le sensor.iopool et les répartir dans différentes entités de type sensor et binary_sensor :

  • sensor.temperature_iopool_pool
  • sensor.ph_iopool_pool
  • sensor.orp_iopool_pool
  • sensor.recommanded_filtration_iopool_pool
  • sensor.mode_iopool_pool
  • binary_sensor.required_actions_iopool_pool

Chacune des entités ayant un unique_id vous pouvez modifier le nom de l'entité par l'interface web de Home Assistant afin d'avoir des noms plus compréhensible. Je vous propose les noms suivants :

  • Température Sonde Piscine
  • pH Sonde Piscine
  • Capacité de désinfection Sonde Piscine
  • Recommandation Durée de Filtration Sonde Piscine
  • Mode Sonde Piscine
  • Actions requises Sonde Piscine
iopool:
  sensor:
    - platform: rest
      unique_id: fabc1ee2-0bbe-416e-b23d-2474ac25fe4e
      name: iopool
      resource: https://api.iopool.com/v1/pool/<votre_id_de_bassin_ici>
      value_template: "{{ value_json.title }}"
      json_attributes:
        - id
        - latestMeasure
        - hasAnActionRequired
        - advice
        - mode
      headers:
        x-api-key: !secret iopool_api_key
      scan_interval: 300
      icon: mdi:pool

      
  template:
    - sensor:
        - name: "temperature_iopool_pool"
          unique_id: b336b008-dc88-4e3b-afd9-d662979fb0c1$
          state: "{{ state_attr('sensor.iopool', 'latestMeasure')['temperature'] | round(2) }}"
          device_class: temperature
          unit_of_measurement: "°C"
          state_class: measurement
          icon: mdi:pool-thermometer
          attributes:
            source: "{{ state_attr('sensor.iopool', 'latestMeasure')['mode'] }}"
            isValid: "{{ state_attr('sensor.iopool', 'latestMeasure')['isValid'] }}"
            measuredAt: "{{ state_attr('sensor.iopool', 'latestMeasure')['measuredAt'] }}"
            
        - name: "ph_iopool_pool"
          unique_id: f4804a67-1224-4507-a4fb-21d983958b7c
          state: "{{ state_attr('sensor.iopool', 'latestMeasure')['ph'] | round(1) }}"
          unit_of_measurement: "pH"
          attributes:
            source: "{{ state_attr('sensor.iopool', 'latestMeasure')['mode'] }}"
            isValid: "{{ state_attr('sensor.iopool', 'latestMeasure')['isValid'] }}"
            measuredAt: "{{ state_attr('sensor.iopool', 'latestMeasure')['measuredAt'] }}"
            
        - name: "orp_iopool_pool"
          unique_id: e0ef9122-c53a-41ae-be72-517f3fcbb443
          state: "{{ state_attr('sensor.iopool', 'latestMeasure')['orp'] | round(0) }}"
          unit_of_measurement: "mV"
          attributes:
            source: "{{ state_attr('sensor.iopool', 'latestMeasure')['mode'] }}"
            isValid: "{{ state_attr('sensor.iopool', 'latestMeasure')['isValid'] }}"
            measuredAt: "{{ state_attr('sensor.iopool', 'latestMeasure')['measuredAt'] }}"
            
        - name: "recommanded_filtration_iopool_pool"
          unique_id: f53659ba-922f-4861-9198-73a7dd43ae6a
          state: "{{ state_attr('sensor.iopool', 'advice')['filtrationDuration'] * 60 }}"
          device_class: duration
          unit_of_measurement: "min"
          icon: mdi:sun-clock-outline
          
        - name: "mode_iopool_pool"
          unique_id: af6db587-be33-44e7-950c-fa52f0453d1f
          state: "{{ state_attr('sensor.iopool', 'mode') }}"
          icon: mdi:auto-mode

    - binary_sensor:
        - name: "required_actions_iopool_pool"
          unique_id: fb6bb7e0-86ad-4f27-90ee-47c39db0ab12
          state: "{{ state_attr('sensor.iopool', 'hasAnActionRequired') }}"
          device_class: problem
          icon: mdi:checkbox-marked-circle-plus-outline

Version complète du fichier

Il vous reste désormais à vérifier votre fichier de configuration par l'interface web Home Assistant et à le redémarrer pour que la configuration s'applique.

Vous devriez désormais avoir l'ensemble des entités que nous avons créées qui ont été remplies avec les informations de votre sonde iopool.

Un dashboard ?

Voici un exemple de carte pour voir l'état de votre piscine à partir des données iopool :

type: vertical-stack
cards:
  - type: custom:mushroom-title-card
    title: Piscine
    subtitle: '{{ states(''sensor.iopool'') }}'
    alignment: center
    title_tap_action:
      action: none
    subtitle_tap_action:
      action: none
  - type: grid
    square: false
    cards:
      - type: custom:button-card
        name: Mode de la sonde
        icon: mdi:pool
        entity: sensor.mode_iopool_pool
        show_state: true
        state:
          - operator: '=='
            value: STANDARD
            styles:
              img_cell:
                - background: var(--green)
          - operator: regex
            value: (ACTIVE_)?WINTER
            icon: mdi:snowflake
            styles:
              img_cell:
                - background: var(--blue-tint)
        styles:
          grid:
            - grid-template-areas: '"n btn" "s btn" "i btn"'
            - grid-template-columns: 1fr min-content
            - grid-template-rows: min-content min-content 1fr
          img_cell:
            - justify-content: start
            - position: absolute
            - width: 130px
            - height: 130px
            - left: 0
            - bottom: 0
            - margin: 0 0 -30px -30px
            - background: var(--blue)
            - border-radius: 200px
          icon:
            - width: 60px
            - left: 35px
            - color: contrast20
            - opacity: '0.6'
          card:
            - height: 100%
            - padding: 22px 8px 22px 22px
          name:
            - justify-self: start
            - align-self: start
            - font-size: 16px
            - font-weight: 500
            - color: contrast20
          state:
            - min-height: 80px
            - justify-self: start
            - align-self: start
            - font-size: 14px
            - opacity: '0.7'
      - type: custom:button-card
        name: Actions Requises
        icon: mdi:emoticon-cool-outline
        entity: binary_sensor.required_actions_iopool_pool
        show_state: true
        state:
          - value: 'off'
            styles:
              img_cell:
                - background: var(--green)
          - value: 'on'
            icon: mdi:exclamation
            styles:
              img_cell:
                - background: var(--red)
        styles:
          grid:
            - grid-template-areas: '"n btn" "s btn" "i btn"'
            - grid-template-columns: 1fr min-content
            - grid-template-rows: min-content min-content 1fr
          img_cell:
            - justify-content: start
            - position: absolute
            - width: 130px
            - height: 130px
            - left: 0
            - bottom: 0
            - margin: 0 0 -30px -30px
            - background: var(--blue)
            - border-radius: 200px
          icon:
            - width: 60px
            - left: 35px
            - color: contrast20
            - opacity: '0.6'
          card:
            - height: 100%
            - padding: 22px 8px 22px 22px
          name:
            - justify-self: start
            - align-self: start
            - font-size: 16px
            - font-weight: 500
            - color: contrast20
          state:
            - min-height: 80px
            - justify-self: start
            - align-self: start
            - font-size: 14px
            - opacity: '0.7'
    columns: 2
  - type: grid
    square: false
    cards:
      - type: custom:button-card
        name: Filtration
        icon: mdi:water-boiler
        entity: input_select.pool_mode
        show_state: true
        custom_fields:
          btn:
            card:
              type: custom:mushroom-chips-card
              chips:
                - type: template
                  tap_action:
                    action: call-service
                    service: input_select.select_option
                    target:
                      entity_id: input_select.pool_mode
                    data:
                      option: Standard
                  hold_action:
                    action: none
                  double_tap_action:
                    action: none
                  icon: mdi:white-balance-sunny
                  entity: input_select.pool_mode
                  card_mod:
                    style: |
                      ha-card {
                        --chip-background: {{ 'var(--green)' if is_state('input_select.pool_mode', 'Standard') else 'var(--contrast4)' }};
                        padding: 5px!important;
                        border-radius: 100px!important;
                      }
                      ha-card::after {
                        content: "Standard";
                        position: absolute;
                        bottom: 10%;
                        left: 10%;
                        transform: translateX(-100%);
                        background-color: var(--contrast1);
                        color: var(--contrast10);
                        font-size: 14px;
                        padding: 5px;
                        border-radius: 3px;
                        white-space: nowrap;
                        display: none;
                      }
                      ha-card:hover::after {
                        display: block;
                      }
                - type: template
                  tap_action:
                    action: call-service
                    service: input_select.select_option
                    target:
                      entity_id: input_select.pool_mode
                    data:
                      option: Active-Winter
                  hold_action:
                    action: none
                  double_tap_action:
                    action: none
                  icon: mdi:sun-snowflake
                  entity: input_select.pool_mode
                  card_mod:
                    style: |
                      ha-card {
                        --chip-background: {{ 'var(--green)' if is_state('input_select.pool_mode', 'Active-Winter') else 'var(--contrast4)' }};
                        padding: 5px!important;
                        border-radius: 100px!important;
                      }
                      ha-card::after {
                        content: "Active-Winter";
                        position: absolute;
                        bottom: 10%;
                        left: 10%;
                        transform: translateX(-100%);
                        background-color: var(--contrast1);
                        color: var(--contrast10);
                        font-size: 14px;
                        padding: 5px;
                        border-radius: 3px;
                        white-space: nowrap;
                        display: none;
                      }
                      ha-card:hover::after {
                        display: block;
                      }
                - type: template
                  tap_action:
                    action: call-service
                    service: input_select.select_option
                    target:
                      entity_id: input_select.pool_mode
                    data:
                      option: Passive-Winter
                  hold_action:
                    action: none
                  double_tap_action:
                    action: none
                  icon: mdi:snowflake
                  entity: input_select.pool_mode
                  card_mod:
                    style: |
                      ha-card {
                        --chip-background: {{ 'var(--green)' if is_state('input_select.pool_mode', 'Passive-Winter') else 'var(--contrast4)' }};
                        padding: 5px!important;
                        border-radius: 100px!important;
                      }
                      ha-card::after {
                        content: "Passive-Winter";
                        position: absolute;
                        bottom: 10%;
                        left: 10%;
                        transform: translateX(-100%);
                        background-color: var(--contrast1);
                        color: var(--contrast10);
                        font-size: 14px;
                        padding: 5px;
                        border-radius: 3px;
                        white-space: nowrap;
                        display: none;
                      }
                      ha-card:hover::after {
                        display: block;
                      }
        styles:
          grid:
            - grid-template-areas: '"n btn" "s btn" "i btn"'
            - grid-template-columns: 1fr min-content
            - grid-template-rows: min-content min-content 1fr
          img_cell:
            - justify-content: start
            - position: absolute
            - width: 130px
            - height: 130px
            - left: 0
            - bottom: 0
            - margin: 0 0 -30px -30px
            - background: var(--blue)
            - border-radius: 200px
          icon:
            - width: 60px
            - color: black
            - opacity: '0.6'
          card:
            - padding: 22px 8px 22px 22px
          name:
            - justify-self: start
            - align-self: start
            - font-size: 16px
            - font-weight: 500
            - color: contrast20
          state:
            - min-height: 80px
            - justify-self: start
            - align-self: start
            - font-size: 14px
            - opacity: '0.7'
          custom_field:
            btn:
              - justify-content: end
              - align-self: start
      - type: custom:button-card
        entity: switch.pool_switch
        name: Pompe Piscine
        icon: mdi:water-boiler
        tap_action:
          action: toggle
        state:
          - value: 'off'
            icon: mdi:water-boiler-off
            styles:
              card:
                - background: var(--red)
              icon:
                - color: var(--contrast1)
              name:
                - color: var(--contrast1)
              custom_fields:
                state:
                  - color: var(--contrast1)
        styles:
          grid:
            - grid-template-areas: '"i i" "n n" "state icon"'
            - grid-template-columns: 1fr 1fr
            - grid-template-rows: 1fr min-content min-content
          card:
            - padding: 20px
            - background: var(--green)
            - height: 100%
          name:
            - justify-self: start
            - font-size: 14px
            - color: var(--black1)
            - opacity: '0.7'
            - padding: 2px 0px
          icon:
            - width: 24px
            - color: var(--black1)
          img_cell:
            - justify-self: start
            - width: 24px
            - height: 24px
            - padding-bottom: 18px
          custom_fields:
            icon:
              - justify-self: end
              - margin-top: '-9px'
            state:
              - justify-self: start
              - font-size: 16px
              - font-weight: 500
              - margin-top: '-9px'
              - color: var(--black1)
        custom_fields:
          icon: |
            [[[
              var state = entity.state;
              if (state == "on")
                return '<ha-icon icon="mdi:toggle-switch" style="color: var(--contrast1); width: 50px; height: 50px">'
              else
                return '<ha-icon icon="mdi:toggle-switch-off" style="color: var(--contrast1); width: 50px; height: 50px">'
            ]]]
          state: |
            [[[
              var state = entity.state;
              if(state == "on")
                return `On`
              else
                return 'Off'
            ]]]
    columns: 2
  - type: custom:button-card
    entity: sensor.pool_elapsed_filtration_duration
    name: Filtration
    icon: mdi:water-boiler
    action: more-info
    styles:
      card:
        - padding: 20px
        - height: 140px
      grid:
        - grid-template-areas: '"i recommanded" "n stat2" "bar bar" "stat1 stat3"'
        - grid-template-columns: 1fr 1fr
        - grid-template-rows: 50px min-content 30px min-content
      name:
        - justify-self: start
        - font-size: 14px
        - opacity: 0.7
      icon:
        - width: 24px
      img_cell:
        - justify-self: start
        - align-self: start
        - width: 34px
        - height: 34px
      custom_fields:
        recommanded:
          - justify-self: end
          - align-self: center
          - padding-bottom: 6px
          - font-size: 12px
          - font-weight: 500
        stat1:
          - justify-self: start
          - font-size: 10px
          - opacity: 0.7
        stat2:
          - justify-self: end
          - font-size: 12px
          - opacity: 0.7
          - font-weight: 500
        stat3:
          - justify-self: end
          - font-size: 10px
          - opacity: 0.7
        bar:
          - justify-self: start
          - margin-top: '-4px'
          - width: 100%
          - border-radius: 6px
          - background: var(--contrast5)
          - height: 12px
    custom_fields:
      recommanded: |
        [[[
          var iopool_recommanded = states['sensor.recommanded_filtration_iopool_pool'].state;
          return "Recommandation iopool: " + new Date(iopool_recommanded * 60 * 1000).toISOString().substr(11, 8);
        ]]]
      bar: |
        [[[
          var duration = states['sensor.pool_pump_calculated_duration'].state;
          var elapsed = Math.round(entity.state * 60, 2);
          var elapsed_percent = Math.round(Math.min(100,((elapsed/duration)*100)),2);
          return `<div> <div style="background: var(--blue); height: 12px; width: ${elapsed_percent}%"></div> </div>`
        ]]]
      stat1: '00:00:00'
      stat2: |
        [[[
          var duration = states['sensor.pool_pump_calculated_duration'].state;
          var elapsed = Math.round(entity.state * 60, 2);
          var elapsed_percent = Math.round(((elapsed/duration)*100),2);
          return "Effectuée: " + new Date(elapsed * 60 * 1000).toISOString().substr(11,8) + " / " + elapsed_percent + "%";
        ]]]
      stat3: >
        [[[return new Date(states['sensor.pool_pump_calculated_duration'].state
        * 60 * 1000).toISOString().substr(11, 8); ]]]
  - type: conditional
    conditions:
      - condition: state
        entity: input_select.pool_mode
        state: Standard
    card:
      type: custom:button-card
      entity: timer.pool_boost
      name: Boost
      icon: mdi:plus-box-multiple
      action: more-info
      state:
        - value: idle
          styles:
            custom_fields:
              name:
                - display: none
              bar:
                - display: none
              stat1:
                - display: none
              stat2:
                - display: none
              stat3:
                - display: none
        - value: active
          styles:
            card:
              - background: var(--green)
            name:
              - color: var(--black1)
            icon:
              - color: var(--black1)
      styles:
        card:
          - padding: 20px
        grid:
          - grid-template-areas: '"i btn" "n stat2" "bar bar" "stat1 stat3"'
          - grid-template-columns: 1fr 4fr
          - grid-template-rows: min-content min-content 30px min-content
        name:
          - justify-self: start
          - padding-top: 10px
          - font-size: 14px
          - opacity: 0.7
          - color: var(--contrast20)
        icon:
          - width: 24px
          - color: var(--contrast20)
        img_cell:
          - justify-self: start
          - align-self: start
          - width: 34px
          - height: 34px
        custom_fields:
          btn:
            - justify-self: end
            - align-self: start
          stat1:
            - justify-self: start
            - font-size: 10px
            - opacity: 0.7
            - color: var(--black1)
          stat2:
            - justify-self: end
            - padding-top: 10px
            - font-size: 12px
            - opacity: 0.7
            - font-weight: 500
            - color: var(--black1)
          stat3:
            - justify-self: end
            - font-size: 10px
            - opacity: 0.7
            - color: var(--black1)
          bar:
            - justify-self: start
            - margin-top: '-4px'
            - width: 100%
            - border-radius: 6px
            - background: var(--contrast5)
            - height: 12px
      custom_fields:
        btn:
          card:
            type: custom:mushroom-chips-card
            chips:
              - type: template
                tap_action:
                  action: call-service
                  service: input_select.select_option
                  target:
                    entity_id: input_select.pool_boost_selector
                  data:
                    option: None
                hold_action:
                  action: none
                double_tap_action:
                  action: none
                icon: mdi:timer-stop
                entity: input_select.pool_boost_selector
                card_mod:
                  style: |
                    ha-card {
                      --chip-background: {{ 'var(--green)' if is_state('input_select.pool_boost_selector', 'None') else 'var(--contrast4)' }};
                      --chip-box-shadow: {{ 'inset 0 0 0 2px var(--white1)' if is_state('input_select.pool_boost_selector', 'None') else 'none' }};
                      padding: 5px!important;
                      border-radius: 100px!important;
                    }
                    ha-card::after {
                      content: "Arrêt du boost";
                      position: absolute;
                      bottom: 10%;
                      left: 10%;
                      transform: translateX(-100%);
                      background-color: var(--contrast1);
                      color: var(--contrast10);
                      font-size: 14px;
                      padding: 5px;
                      border-radius: 3px;
                      white-space: nowrap;
                      display: none;
                    }
                    ha-card:hover::after {
                      display: block;
                    }
              - type: template
                tap_action:
                  action: call-service
                  service: input_select.select_option
                  target:
                    entity_id: input_select.pool_boost_selector
                  data:
                    option: 1H
                hold_action:
                  action: none
                double_tap_action:
                  action: none
                icon: mdi:numeric-1
                entity: input_select.pool_boost_selector
                card_mod:
                  style: |
                    ha-card {
                      --chip-background: {{ 'var(--green)' if is_state('input_select.pool_boost_selector', '1H') else 'var(--contrast4)' }};
                      --chip-box-shadow: {{ 'inset 0 0 0 2px var(--white1)' if is_state('input_select.pool_boost_selector', '1H') else 'none' }};
                      padding: 5px!important;
                      border-radius: 100px!important;
                    }
                    ha-card::after {
                      content: "Boost 1H";
                      position: absolute;
                      bottom: 10%;
                      left: 10%;
                      transform: translateX(-100%);
                      background-color: var(--contrast1);
                      color: var(--contrast10);
                      font-size: 14px;
                      padding: 5px;
                      border-radius: 3px;
                      white-space: nowrap;
                      display: none;
                    }
                    ha-card:hover::after {
                      display: block;
                    }
              - type: template
                tap_action:
                  action: call-service
                  service: input_select.select_option
                  target:
                    entity_id: input_select.pool_boost_selector
                  data:
                    option: 4H
                hold_action:
                  action: none
                double_tap_action:
                  action: none
                icon: mdi:numeric-4
                entity: input_select.pool_boost_selector
                card_mod:
                  style: |
                    ha-card {
                      --chip-background: {{ 'var(--green)' if is_state('input_select.pool_boost_selector', '4H') else 'var(--contrast4)' }};
                      --chip-box-shadow: {{ 'inset 0 0 0 2px var(--white1)' if is_state('input_select.pool_boost_selector', '4H') else 'none' }};
                      padding: 5px!important;
                      border-radius: 100px!important;
                    }
                    ha-card::after {
                      content: "Boost 4H";
                      position: absolute;
                      bottom: 10%;
                      left: 10%;
                      transform: translateX(-100%);
                      background-color: var(--contrast1);
                      color: var(--contrast10);
                      font-size: 14px;
                      padding: 5px;
                      border-radius: 3px;
                      white-space: nowrap;
                      display: none;
                    }
                    ha-card:hover::after {
                      display: block;
                    }
              - type: template
                tap_action:
                  action: call-service
                  service: input_select.select_option
                  target:
                    entity_id: input_select.pool_boost_selector
                  data:
                    option: 8H
                hold_action:
                  action: none
                double_tap_action:
                  action: none
                icon: mdi:numeric-8
                entity: input_select.pool_boost_selector
                card_mod:
                  style: |
                    ha-card {
                      --chip-background: {{ 'var(--green)' if is_state('input_select.pool_boost_selector', '8H') else 'var(--contrast4)' }};
                      --chip-box-shadow: {{ 'inset 0 0 0 2px var(--white1)' if is_state('input_select.pool_boost_selector', '8H') else 'none' }};
                      padding: 5px!important;
                      border-radius: 100px!important;
                    }
                    ha-card::after {
                      content: "Boost 8H";
                      position: absolute;
                      bottom: 10%;
                      left: 10%;
                      transform: translateX(-100%);
                      background-color: var(--contrast1);
                      color: var(--contrast10);
                      font-size: 14px;
                      padding: 5px;
                      border-radius: 3px;
                      white-space: nowrap;
                      display: none;
                    }
                    ha-card:hover::after {
                      display: block;
                    }
              - type: template
                tap_action:
                  action: call-service
                  service: input_select.select_option
                  target:
                    entity_id: input_select.pool_boost_selector
                  data:
                    option: 24H
                hold_action:
                  action: none
                double_tap_action:
                  action: none
                icon: mdi:hours-24
                entity: input_select.pool_boost_selector
                card_mod:
                  style: |
                    ha-card {
                      --chip-background: {{ 'var(--green)' if is_state('input_select.pool_boost_selector', '24H') else 'var(--contrast4)' }};
                      --chip-box-shadow: {{ 'inset 0 0 0 2px var(--contrast1)' if is_state('input_select.pool_boost_selector', '24H') else 'none' }};
                      padding: 5px!important;
                      border-radius: 100px!important;
                    }
                    ha-card::after {
                      content: "Boost 24H";
                      position: absolute;
                      bottom: 10%;
                      left: 10%;
                      transform: translateX(-100%);
                      background-color: var(--contrast1);
                      color: var(--contrast10);
                      font-size: 14px;
                      padding: 5px;
                      border-radius: 3px;
                      white-space: nowrap;
                      display: none;
                    }
                    ha-card:hover::after {
                      display: block;
                    }
        bar: |
          [[[
            if (entity.state == "active") {
              var duration = entity.attributes.duration;
              var durationSeconds = duration.split(':').reduce((acc, time) => (60 * acc) + +time, 0);
              var finishesAt = entity.attributes.finishes_at;
              var now = new Date();
              var remainingSeconds = Math.floor((new Date(finishesAt).getTime() - now.getTime()) / 1000);
              var remainingPercent = Math.round(((remainingSeconds/durationSeconds)*100),2);
              return `<div> <div style="background: var(--blue); height: 12px; width: ${remainingPercent}%"></div> </div>`
            }
          ]]]
        stat1: '00:00:00'
        stat2: |
          [[[
            if (entity.state == "active") {
              var finishesAt = entity.attributes.finishes_at;
              var now = new Date();
              var remainingSeconds = Math.floor((new Date(finishesAt).getTime() - now.getTime()) / 1000);
              return new Date(remainingSeconds * 1000).toISOString().substr(11, 8);
            }
          ]]]
        stat3: |
          [[[
            return entity.attributes.duration
          ]]]
  - type: conditional
    conditions:
      - entity: sensor.mode_iopool_pool
        state: STANDARD
    card:
      type: custom:vertical-stack-in-card
      cards:
        - type: custom:mini-graph-card
          entities:
            - entity: sensor.temperature_iopool_pool
              name: Température du bassin
          hours_to_show: 96
          animate: true
          line_width: 5
          group_by: hour
          state_adaptive_color: true
          hour24: true
          decimals: 1
          show:
            extrema: true
            average: true
            labels: false
          color_thresholds:
            - value: 20
              color: '#44739e'
            - value: 24
              color: '#12f33f'
            - value: 30
              color: '#f39c12'
            - value: 32
              color: '#c0392b'
        - type: custom:pool-monitor-card
          title: Analyse de l'eau
          temperature: sensor.temperature_iopool_pool
          temperature_setpoint: 27
          temperature_step: 4
          ph: sensor.ph_iopool_pool
          ph_setpoint: 7.4
          ph_step: 0.3
          orp: sensor.orp_iopool_pool
          orp_setpoint: 725
          orp_step: 75
          show_labels: true
          language: fr
Exemple de composition d'une carte Piscine

Plusieurs types de cartes sont utilisés et nécéssaires pour composer cette carte :

⚠️
J'utilise le thème Rounded qui inclus des couleurs pré-définis. Vous devrez ajuster les couleurs utilisant le format var(--XXXX) avec une couleurs spécifiques ou utilisées dans votre thème.

On va un peu plus loin ?

Nous avons désormais dans notre Home Assistant, l'état de santé de notre piscine. Nous pouvons mettre en place des automatisations pour nous prévenir lorsqu'il y a des actions d'entretien à effectuer dans notre piscine.

Mais vous le savez tout comme moi, une piscine bien entretenue est une piscine bien filtrée.

Il existe un sempiternel débat sur le calcul du temps de filtration. Certains appliquent le calcul de la température de l'eau divisé par deux.

blog.piscineco.fr
Est-ce la bonne méthode ?

D'autres se disent que cette méthode ne peut pas marcher dans tous les cas, car elle dépend de la taille de votre bassin et de la puissance de votre pompe. Je ne saurai dire s'il y a une bonne ou une mauvaise méthode.

Par chance, afin d'éviter tout dilemme dans un choix, iopool calcule pour vous le temps de filtration recommandé, dépendant de la température de la piscine, mais aussi d'autres facteurs. J'utilise le temps de filtration recommandé par iopool depuis plusieurs années et mon eau est restée cristalline.

Alors pourquoi ne pas utiliser cette durée de filtration recommandée par iopool pour piloter notre pompe de piscine automatiquement ?

Filtrer sa piscine automatiquement

Comment faire ?

Partant du constat précédent, j'ai développé sur ma plateforme domotique précédente (chut... on ne va pas prononcer son nom 😉) un plugin pour iopool qui incluait les fonctions de filtration automatisée.

Tout naturellement, en transitant vers notre cher Home Assistant, j'ai voulu reproduire la même chose. Mais je n'ai malheureusement rien trouvé qui me convienne totalement. Je suis quelqu'un de compliqué…

Encore nouveau à l'époque sur Home Assistant, une intégration ne me semblait pas encore à ma portée (cela ne l'est pas encore, mais je vais m'y atteler dans les mois à venir pour éventuellement proposer une intégration iopool et vous simplifier la tâche à l'avenir). Mais développer en Python n'était pas vraiment un problème…

C'est là qu'entre en jeu AppDaemon et le projet iopool Pump Manager.

iopool Pump Manager logo

iopool Pump Manager

iopool Pump Manager est une application AppDaemon disponible sur HACS.

Elle vous permet de piloter votre pompe de piscine en prenant en compte les recommandations de temps de filtration fournies par iopool. Mais pas seulement.

En effet, on peut définir :

  • Un seuil minimal ou maximal de filtration au-dessus de la durée de filtration recommandée. Ainsi, je peux m'assurer que malgré les recommandations iopool ma pompe filtrera au moins et/ou au plus une certaine durée dans la journée.
  • définir plusieurs périodes de filtration dans la journée (j'aime bien en avoir une pendant les heures chaudes, mais aussi une après le bain quotidien, mais je déteste faire tourner la pompe pour rien, donc une bonne répartition est bien pratique).
  • Évoluer avec les recommandations de filtration. En effet, si je définis une période de filtration le matin, iopool peut me recommander de filtrer durant 3 heures. Mais à 16h il me recommandera peut-être 6h de filtration. Il me manquera donc 3h. En positionnant 2 périodes, la dernière période prendra toujours en compte le temps de filtration déjà effectué pour compléter (si nécessaire) le temps de filtration recommandé.
  • Gérer les boosts. Lorsque vous mettez des produits dans votre piscine, il faut faire circuler l'eau durant un certain temps afin que le produit se mélange bien. Il est bien utile de pouvoir demander un allumage de la pompe pour une durée de boost définie afin qu'elle s'arrête quand la filtration n'est plus nécessaire.
  • La génération d'un évènement en fin de boost ou de filtration quotidienne pour savoir si elle a tourné suffisamment par rapport à ce qui était demandé.

Installation

Toute l'installation est plutôt bien décrite dans la documentation officielle (en anglais).

Je ne rentrerais pas dans l'ensemble des détails dans cet article mais vous pouvez regarder la vidéo pour cela.

L'objectif sera ici de définir les grandes étapes pour avoir quelque chose d'opérationnel rapidement. N'hésitez pas à consulter la documentation en ligne pour plus de précision.

  1. Il faut avoir un HACS fonctionnel sur son Home Assistant
  2. Installer AppDaemon en tant qu'addon (ou une autre méthode qui vous conviendra, mais nous continuerons avec l'exemple de l'addon)
  3. Activer les applications AppDaemon dans HACS
  4. Ajouter l'application iopool Pump Manager dans HACS (en tant qu'automation) : https://github.com/mguyard/appdaemon-iopoolpumpmanager
  5. Téléchargez l'application iopool Pump Manager dans HACS
  6. Mettre à jour la configuration initiale de AppDaemon avec votre éditeur habituel
  7. Après avoir ajouté précédemment le package pour les entités iopool, il vous faut ajouter quelques entités utiles pour iopool Pump Manager. Pour cela, nous allons créer un nouveau package.
  8. Mise en place de la configuration pour l'app iopool Pump Manager. Plus de précision sur les paramètres sont disponibles ici.
  9. Si comme moi, vous aimez mettre en place les notifications pour savoir quand votre boost s'arrête ou bien quand votre filtration quotidienne se termine pour avoir toutes les informations utiles telles que la durée de filtration effective, tout est détaillé ici.
ℹ️
L'étape 4 sera bientôt facultative, l'intégration de iopool Pump Manager dans les repos officiels étant en attente de validation. Une fois la validation effectuée, il suffira de rechercher iopool Pump Manager dans les automations HACS.

C'est toujours mieux en vidéo

Si vous voulez vous faire guider au son de ma jolie voix, alors suivez cette vidéo disponible sur la chaîne Youtube HACF

Tutoriel Vidéo iopool et iopool Pump Manager

Une questions ?

N'hésitez pas à venir poser vos questions sur le forum HACF