Water Meter

Mon installation personnelle pour suivre la consommation d’eau dans Home Assistant à partir d’un compteur analogique, avec une caméra ESP32, ESPHome, Docker et une lecture d’image basée sur de l’IA.

Introduction

Au départ, j’utilisais le projet AI-on-the-edge-device. Il fonctionnait bien et m’a permis de valider l’idée assez rapidement.

Avec le temps, la fiabilité est devenue plus délicate chez moi: dès que la caméra bougeait un peu, même légèrement, la lecture pouvait devenir moins stable. J’ai donc créé ma propre approche basée sur de l’IA, avec l’objectif d’être plus tolérant aux petits décalages et plus fiable dans mon installation.

Pré-requis

Voici le matériel et les services que j’utilise. L’installation MQTT n’est pas détaillée ici: je pars du principe qu’un serveur MQTT est déjà disponible et configuré.

  • Une carte caméra ESP32.
  • Un module caméra OV2640.
  • Important: le module OV3660 n’est pas compatible.
  • Home Assistant.
  • Un environnement Docker / Docker Compose.
  • Un serveur MQTT.
  • Optionnel: une imprimante 3D pour le support de montage.

Pour fixer la caméra devant le compteur, j’utilise ce support imprimé en 3D: Watermeter Camera Mount sur Thingiverse.

Configuration ESPHome

L’ESP32 est flashé avec ESPHome. Une fois configuré, le flux de la caméra et le flash deviennent disponibles dans Home Assistant. Dans mon cas, l’entité de flash ressemble à light.watermeter_watermeter_flash.

Cette entité est utilisée plus tard par l’application WaterMeter pour éclairer le compteur avant de prendre une capture. Voici la configuration ESPHome utilisée pour exposer la caméra en snapshot sur le port 8080 et piloter le flash.

watermeter.yaml
esphome:
  name: esphome-web-a80724
  friendly_name: Watermeter
  name_add_mac_suffix: false

esp32:
  board: esp32cam
  framework:
    type: arduino

psram:
  mode: quad
  speed: 40MHz

logger:

api:

ota:
  platform: esphome

wifi:
  ssid: !secret wifi_ssid
  password: !secret wifi_password

esp32_camera:
  name: Watermeter Cam
  external_clock:
    pin: GPIO0
    frequency: 20MHz
  i2c_pins:
    sda: GPIO26
    scl: GPIO27
  data_pins: [GPIO5, GPIO18, GPIO19, GPIO21, GPIO36, GPIO39, GPIO34, GPIO35]
  vsync_pin: GPIO25
  href_pin: GPIO23
  pixel_clock_pin: GPIO22
  power_down_pin: GPIO32
  resolution: 640x480
  jpeg_quality: 12

esp32_camera_web_server:
  - port: 8080
    mode: snapshot

output:
  - platform: gpio
    pin: GPIO4
    id: cam_flash

light:
  - platform: binary
    name: "Watermeter Flash"
    output: cam_flash

Conteneurs Docker

L’installation utilise deux conteneurs: WaterMeter, qui orchestre les captures, Home Assistant et MQTT, et DigitVisionAI, qui lit les chiffres sur l’image.

docker-compose.yml
services:
  digit-vision-ai:
    image: ghcr.io/jerome-marquis/digitvisionai:latest
    container_name: digit-vision-ai
    ports:
      - "8000:8000"
    volumes:
      - ./models:/app/models:ro
    environment:
      YOLO_WINDOW_MODEL: /app/models/digit-vision-ai-roi-2026010401.pt
      YOLO_DIGITS_MODEL: /app/models/digit-vision-ai-crop-digits-2026051601.pt
      CONF_WINDOW: "0.5"
      CONF_DIGITS: "0.5"
      IOU: "0.3"
      EXPECTED_DIGITS: "7"
      CLUSTER_GAP_RATIO: "0.6"
    labels:
      - "com.centurylinklabs.watchtower.enable=true"
    restart: unless-stopped

  watermeter:
    image: ghcr.io/jerome-marquis/watermeter:latest
    container_name: watermeter
    depends_on:
      - digit-vision-ai
    volumes:
      - ./data:/data
    environment:
      INITIAL_WATER_LITERS: "<INITIAL_COUNTER_VALUE_IN_LITERS>"
      DISCOVERY_INTERVAL_SECONDS: "600"

      INTERVAL_DAY_SECONDS: "60"
      INTERVAL_NIGHT_SECONDS: "1800"
      NIGHT_START_HOUR: "23"
      NIGHT_END_HOUR: "6"
      MAX_DELTA_DAY_L: "50"
      MAX_DELTA_NIGHT_L: "100"
      OPENAI_EXPECTED_DIGITS: "7"

      HA_URL: "http://<HOME_ASSISTANT_HOST>:8123"
      HA_TOKEN: "<HOME_ASSISTANT_LONG_LIVED_ACCESS_TOKEN>"
      FLASH_ENTITY: "light.watermeter_watermeter_flash"
      SNAPSHOT_URL: "http://<ESP32_CAMERA_HOST>:8080/"

      FLASH_PRE_SECONDS: "3"
      FLASH_POST_SECONDS: "3"
      SNAPSHOT_RETRY_COUNT: "5"
      SNAPSHOT_MIN_BRIGHTNESS: "25"
      MAX_IMAGES: "500"

      MQTT_HOST: "<MQTT_HOST>"
      MQTT_PORT: "1883"
      MQTT_USER: "<MQTT_USER>"
      MQTT_PASS: "<MQTT_PASSWORD>"
      MQTT_TOPIC: "watermeter/total_l"
      MQTT_RETAIN: "true"

      HA_DISCOVERY: "true"
      HA_DISCOVERY_PREFIX: "homeassistant"
      HA_SENSOR_NAME: "Water Meter Total (AI)"
      HA_SENSOR_ID: "watermeter_total_l_ai"

      AI_PROVIDER: "local"
      LOCAL_AI_ENDPOINT: "http://digit-vision-ai:8000/read"
      LOCAL_AI_TIMEOUT: "15"

      AI_ENDPOINT: "https://api.openai.com/v1/responses"
      AI_API_KEY: "<OPTIONAL_AI_API_KEY>"
      AI_MODEL: "gpt-4.1-mini"
      AI_TIMEOUT: "30"
      AI_RETRIES: "2"
    labels:
      - "com.centurylinklabs.watchtower.enable=true"
    restart: unless-stopped

Téléchargement des modèles IA

DigitVisionAI a besoin de deux modèles pour détecter la zone du compteur puis lire les chiffres. Dans cette installation, ils sont généralement placés dans le dossier ./models monté dans le conteneur Docker.

digit-vision-ai-roi-2026010401.pt

Modèle utilisé pour détecter la zone de lecture du compteur.

Télécharger

digit-vision-ai-crop-digits-2026051601.pt

Modèle utilisé pour lire les chiffres dans la zone détectée.

Télécharger

Variables importantes

Le fichier Docker Compose peut contenir beaucoup de réglages. Voici uniquement les variables que je considère comme les plus importantes pour comprendre l’installation.

DigitVisionAI

YOLO_WINDOW_MODEL
Modèle utilisé pour détecter la zone utile du compteur.
YOLO_DIGITS_MODEL
Modèle utilisé pour lire les chiffres dans la zone détectée.

Les modèles utilisés dans mon installation sont digit-vision-ai-roi-2026010401.pt et digit-vision-ai-crop-digits-2026051601.pt. Ils doivent être placés dans un dossier de modèles approprié, par exemple ./models monté dans le conteneur.

WaterMeter

HA_URL
URL de votre instance Home Assistant.
HA_TOKEN
Token longue durée Home Assistant. À garder privé.
FLASH_ENTITY
Entité du flash ESPHome, par exemple light.watermeter_watermeter_flash.
SNAPSHOT_URL
URL du flux/cliché de la caméra ESP32. Dans mon cas, elle passe généralement par le port :8080.

MQTT

MQTT_HOST
Adresse du serveur MQTT.
MQTT_PORT
Port MQTT, souvent 1883.
MQTT_USER / MQTT_PASS
Identifiants MQTT. À remplacer par vos propres valeurs.
MQTT_TOPIC
Topic utilisé pour publier la valeur totale.
MQTT_RETAIN
Indique si MQTT conserve la dernière valeur publiée.

IA optionnelle

Une IA externe peut servir de solution de secours si la lecture locale rencontre un problème.

AI_ENDPOINT
URL/endpoint de l’IA externe.
AI_API_KEY
Clé API optionnelle. À garder privée.
AI_MODEL
Modèle utilisé par l’API IA externe.

Intégration Home Assistant

Une fois les conteneurs démarrés, le capteur devrait apparaître automatiquement dans Home Assistant via la découverte MQTT. Il peut ensuite être ajouté au tableau de bord Énergie pour suivre la consommation d’eau.

La partie importante est d’avoir une chaîne complète qui fonctionne: ESP32 accessible, flash pilotable, snapshot lisible, IA locale opérationnelle, puis publication MQTT vers Home Assistant.

Galerie à venir

Je compléterai cette section avec de vraies captures et photos de mon installation. Pour l’instant, je garde uniquement les emplacements prévus.

Liens et documentation

Les détails techniques supplémentaires, le code et les évolutions du projet sont disponibles sur GitHub.

Si ce projet vous plaît ou vous rend service, vous pouvez soutenir le temps passé dessus.

☕ M’offrir un café