Automatisation du déploiement d’ESP32 via Ansible

Depuis quelques semaines, j’ai fait pas mal d’automatisation/domotique à la maison, et plus particulièrement de suivi des consommations (eau, gaz, électricité). En faisant cela, j’ai découvert ESPHome, qui simplifie beaucoup la programmation d’ESP32 et leur intégration avec HomeAssistant.

Mais depuis quelques années, j’évite le plus possible de déployer quoi que ce soit manuellement. J’utilise Ansible pour tout automatiser, et réduire énormément le travail nécessaire à redéployer n’importe quel morceau de mon « infra », que cela soit à la maison ou sur mon serveur perso. Du coup, ESPHome seul ne me suffisait pas, et je voulais pouvoir réinstaller un ESP32 d’une seule ligne de commande.

Dans cet article, je vais partager les parties de mon repository Ansible qui me permettent de faire cela, et expliquer à quoi sert chaque partie, en prenant pour exemple l’ESP32 qui me sert à suivre la consommation d’eau à la maison. J’espère que cela pourra intéresser et/ou aider quelqu’un sur internet, même si c’est du Ansible assez basique et que je suppose que n’importe qui qui gère sa maison avec Ansible peut le faire tout·e seul·e. Désolé de ne pas simplement partager un lien Github ou équivalent : c’est gitté localement, et je n’ai pas envie de dupliquer la chose.

Voici donc mon répertoire Ansible :

srv-installer
├── playbook.yml
├── secrets.yml
├── inventory
│   ├── group_vars
│   │   └── lan.yml
│   ├── host_vars
│   │   └── esp-water-meter.lan.yml
│   └── master
└── roles
    ├── esphome
    │   └── tasks
    │       └── main.yml
    └── esp-upload
        ├── tasks
        │   └── main.yml
        └── templates
            └── esp-water-meter.yml.j2

Le fichier inventory/master contient tous les hôtes gérés par Ansible, rangés dans des groupes afin de mutualiser certaines variables. En voici l’extrait qui nous intéresse :

[lan:children]
esp

[esp]
esp-water-meter.lan

Le fichier inventory/group_vars/lan.yml contient quelques variables utilisées dans la maison, comme le nom du réseau Wifi :

wifi_ssid: my-wifi-access-point

Le fichier inventory/host_vars/esp-water-meter.lan.yml contient les variables nécessaires pour l’ESP chargé de surveiller la consommation d’eau, ici seul le modèle d’ESP32 est nécessaire :

esp_model: esp-wrover-kit

Le fichier secrets.yml file contient… les variables « secrètes », chiffrées via ansible-vault (Non, je ne vais pas déployer Hashicorp’s Vault ou quoi que ce soit du genre, ce serait un peu exagéré même pour moi):

wifi_psk: !vault |
          $ANSIBLE_VAULT;1.1;AES256
          6230...464313235613A32423082934579203475920380056639
          1342...464313239859234854657904247890234850348502639

Et voilà pour les variables. On arrive au playbook principal, qui appelle divers rôles pour les diverses machines ou groupes de machines. Voici la partie spécifique aux ESP32 :

- hosts: esp
  vars_files:
    - secrets.yml
  gather_facts: no
  serial: 1
  roles:
    - { role: 'esphome', tags: ['esphome'] }
    - { role: 'esp-upload', tags: ['esp-upload'] }

Je mets gather_facts à no, forcément, étant donné qu’un ESP32 n’a pas de ssh, et encore moins de Python. Vous verrez plus bas que chaque tâche Ansible est déléguée à localhost, car en réalité, tout le travail est fait sur mon laptop.

Je positionne aussi serial à 1, de manière à ce que le playbook joue les rôles en séquence pour chaque ESP32. Cela me permettra d’en brancher un, si nécessaire, via l’adaptateur USB/FTDI. Cela est utile pour le premier déploiement – lors des suivants, les mises à jour se font Over the Air.

Le premier rôle, esphome, est très simple : Il se contente d’installer l’outil esphome sur mon ordinateur. Voici le fichier roles/esphome/tasks/main.yml :

---
- name: install esphome
  pip:
    executable: pip3
    name: esphome
    state: present
  become: yes
  delegate_to: localhost

Le deuxième rôle est celui qui prépare et déploie le fichier Yaml source pour chaque ESP32. Je fais cela avec un template par ESP32, nommé suivant l’ESP32, et cela me permet d’enregistrer le source sans y mettre de modèle, point d’accès Wifi, passphrase Wifi ou quoi que ce soit en dur dans le fichier. Si je renomme mon point d’accès Wifi, par exemple, je n’aurai qu’un fichier à modifier (inventory/group_vars/lan.yml), et non chaque source.

Voici le fichier de tâches pour ce rôle, roles/esp-upload/tasks/main.yml :

- name: build short name
  set_fact:
    short_name: "{{ inventory_hostname | regex_replace('\\.[a-z]+$', '') }}"
  delegate_to: localhost

- name: build source file
  template:
    src: "{{ short_name }}.yml.j2"
    dest: "/tmp/{{ short_name }}.yml"
  delegate_to: localhost

- name: compile source
  shell:
    chdir: /tmp
    cmd: "esphome compile {{ short_name }}.yml"
  delegate_to: localhost

- name: connect ESP
  pause:
    prompt: Please connect ESP to USB FTDI if OTA is not possible
  delegate_to: localhost

- name: upload to esp
  shell:
    chdir: /tmp
    cmd: "esphome upload {{ short_name }}.yml"
  delegate_to: localhost

Tout d’abord, on construit la variable short_name à partir du nom d’hôte. Elle sera utilisée pour choisir le bon fichier source dans les templates, et dans le fichier source lui-même.

Puis on prépare le fichier source définitif avec template, remplaçant les variables par leur valeur. (Le fichier source est plus bas, pour référence), et on le range dans /tmp.

Ensuite, on compile le firmware avec esphome compile.

Après cela, on fait une pause, de manière à me laisser le temps de brancher physiquement l’ESP32 si nécessaire.

Enfin, après que j’aie validé le prompt avec Entrée, on envoie le firmware sur l’ESP32 avec la commande esphome upload.

Dans ce morceau de playbook et d’inventaire, il n’y a qu’un ESP32, celui qui suit ma consommation d’eau. Mais si – non, quand ! – j’en ajouterai d’autres, le playbook continuera ensuite pour le déploiement du suivant.

Pour référence, voici un extrait du yaml source EspHome pour cet ESP32, rangé dans roles/esp-upload/templates/esp-water-meter.yml.j2 :

esphome:
  name: {{ short_name }}

esp32:
  board: {{ esp_model }}
  framework:
    type: arduino

# Enable Home Assistant API
api:
  password: ""

#Yes, I should add passwords on both api and ota.
ota:
  password: ""

wifi:
  ssid: "{{ wifi_ssid }}"
  password: "{{ wifi_psk }}"

sensor:
  - platform: pulse_counter
    id: "pin14_pulse"
    pin:
      number: 14
      inverted: true
      mode:
        input: true
        pullup: true
    name: "pin14_pulse"
    count_mode:
      rising_edge: DISABLE
      falling_edge: INCREMENT
    use_pcnt: false
    internal_filter: 25ms
    total:
      unit_of_measurement: 'L'
      accuracy_decimals: 0
      name: "pin14_liters_index"

Et voilà : je peux maintenant mettre à jour mes ESP32 d’une seule commande :

$ ansible-playbook --ask-become-pass --ask-vault-pass --inventory inventory/ --limit esp playbook.yml 
BECOME password: 
Vault password: 

PLAY [esp] *********************************************************

TASK [install esphome] *********************************************
ok: [esp-water-meter.lan]

TASK [esp-upload : build short name] *******************************
ok: [esp-water-meter.lan]

TASK [esp-upload : build source file] ******************************
changed: [esp-water-meter.lan]

TASK [esp-upload : compile source] *********************************
changed: [esp-water-meter.lan]

TASK [esp-upload : connect ESP] ************************************
[esp-upload : connect ESP]
Please connect ESP to USB FTDI if OTA is not possible:
ok: [esp-water-meter.lan]

TASK [esp-upload : upload to esp] **********************************
changed: [esp-water-meter.lan]

PLAY RECAP *********************************************************
esp-water-meter.lan       : ok=6    changed=3    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

Peut-être qu’il faudrait que je m’applique autant sur la partie matérielle que sur la partie logicielle ?

Je n’ai même pas raccourci les fils des capteurs, j’ai un peu honte.

Pour conclure, voici le résultat, visualisé dans Grafana, après que l’ESP32 commence à envoyer des données dans HomeAssistant, qui les pousse ensuite dans InfluxDB :

(Je sais que l’eau n’est pas une énergie, mais j’avais la flemme de faire un autre dossier Grafana pour un seul dashboard)