Skip to content

Cian911/smart-gas-meter

Repository files navigation

Smart Gas Meter

Smart Gas Meter

A Raspberry Pi pipeline that reads seven-segment LCD gas meter displays from photos using a YOLOv8 object detector, reports readings to Home Assistant, and optionally submits them to Gas Networks Ireland.

How it works

A Pi 4 with Camera Module 3 photographs the meter every 10 minutes via cron. Each image is fed to a fine-tuned YOLOv8n model that:

  1. Detects every digit in the image — outputs a bounding box, class label (0–9), and confidence score for each
  2. Sorts detections left-to-right by bounding box x-centre to reconstruct the reading
  3. Reports the reading to Home Assistant as sensor.gas_meter

No fixed ROI, no column segmentation, no preprocessing chain. One model forward pass produces the complete reading.

Quick start

# Create a virtual environment and install deps
python -m venv venv
source venv/bin/activate
pip install -r requirements.txt

Run inference with the bundled model

A trained model ships with the repo (model_digit_yolo.onnx), along with a calibrated roi_config.json, so you can read an image straight away:

python infer_yolo.py data/meter-captures/your_capture.jpeg --debug

infer_yolo.py uses model_digit_yolo.onnx by default; pass --model to use your own weights. Note the bundled model and ROI are calibrated to my meter and camera framing — for your own meter you'll want to retrain (below).

Train your own model

# Step 0: one-time calibration — draw a box around the display (opens a GUI window)
python calibrate.py
# Output: roi_config.json

# Step 1: label captured images with their meter readings
python label.py
# Output: label_progress.json

# Step 2: convert labels to YOLO dataset format
python prepare_yolo_dataset.py
# Output: yolo_dataset/ (2,584 train / 645 val images + annotations)

# Step 3: fine-tune YOLOv8n (~1 hour on a laptop GPU)
#         downloads the COCO-pretrained yolov8n.pt base model on first run
python train_yolo.py
# Output: model_digit_yolo.pt

# Step 4: run inference with your freshly trained model
python infer_yolo.py data/meter-captures/your_capture.jpeg --debug

calibrate.py and label.py open matplotlib GUI windows, so run them on a machine with a display — not headless over SSH.

Scripts

Script Purpose
calibrate.py One-time GUI calibration → roi_config.json (used for labeling, not inference)
extract.py Batch crop all images in data/meter-captures/data/extracted/
label.py Interactive labeling → label_progress.json
prepare_yolo_dataset.py Convert label_progress.json → YOLO training format
train_yolo.py Fine-tune YOLOv8n on the labeled dataset → model_digit_yolo.pt
infer_yolo.py Production inference: detect digits, POST reading to Home Assistant
submit_gni.py Submit a reading to Gas Networks Ireland
sync_captures.sh Pull new captures from Pi via SCP

Model

YOLOv8n (nano) fine-tuned on 3,229 meter capture images:

Input image → 300px-padded ROI crop → YOLOv8n → [(digit, bbox, conf), ...]
                                                   ↓ sort left-to-right
                                                   → "10872.253 m³"
Property Value
Architecture YOLOv8n (3.2M parameters)
Pre-training COCO (80-class, 120K images)
Fine-tune dataset 2,584 train / 645 val meter images
mAP50 (validation) 0.9950
Precision / Recall 0.9995 / 1.0000
Training model model_digit_yolo.pt (~6 MB)
Deployment model model_digit_yolo.onnx (~12 MB, no PyTorch needed)
Inference time (Pi 4 CPU) ~300–500ms via ONNX Runtime

Training uses transfer learning from COCO pre-trained weights — near-perfect accuracy in a few epochs on a consumer GPU. The model is exported to ONNX for Pi deployment to avoid PyTorch ARM compatibility issues.

Why YOLO instead of a CNN classifier?

An early approach used a CNN to classify pre-cropped 28×28 digit patches. This required:

  • Exact pixel-level calibration of digit column boundaries
  • Per-crop preprocessing (CLAHE, min-max stretch, polarity normalisation)
  • Re-calibration and re-training every time the camera shifted position

The YOLO approach eliminates all of these fragilities. The model finds digits wherever they appear in the image. Camera drift of up to 300px is tolerated automatically because training crops include 300px padding around the display.

Labeling workflow

# 1. Sync new captures from Pi
bash sync_captures.sh

# 2. Extract display crops from new captures
python extract.py

# 3. Label new captures interactively
python label.py
# Shows each display crop; type the 8-digit reading shown

# 4. Regenerate YOLO dataset and retrain
python prepare_yolo_dataset.py
python train_yolo.py

# 5. Deploy updated model to Pi
scp model_digit_yolo.pt pi@raspberrypi.local:/home/pi/meter-ocr/

Data layout

data/
  meter-captures/     raw Pi captures (.jpeg files, not in git)
  extracted/          display crops (output of extract.py)
  test/               held-out images with known readings

label_progress.json   resume-safe map of image filename → 8-digit reading
yolo_dataset/         generated YOLO training data (not in git)
  images/train/
  images/val/
  labels/train/
  labels/val/
  dataset.yaml

Production deployment

See DEPLOYMENT.md for the full Pi setup. The short version:

# Dev machine: export model to ONNX after training
venv/bin/python -c "from ultralytics import YOLO; YOLO('model_digit_yolo.pt').export(format='onnx', imgsz=640, simplify=True)"

# Copy files to Pi
scp infer_yolo.py model_digit_yolo.onnx roi_config.json \
    pi@raspberrypi.local:/home/pi/meter-ocr/

# On the Pi: install runtime (no PyTorch needed)
pip3 install ultralytics onnxruntime opencv-python-headless python-dotenv --break-system-packages

# Run inference on Pi using ONNX model
python3 /home/pi/meter-ocr/infer_yolo.py \
    --model /home/pi/meter-ocr/model_digit_yolo.onnx \
    /home/pi/meter-captures/20260601_152844.jpeg

Home Assistant

The pipeline creates sensor.gas_meter after the first successful reading. Add it as a gas source under Settings → Energy → Gas consumption to track usage in the Energy dashboard.

Attribute Value
state reading in m³ (e.g. 10872.253)
unit_of_measurement
device_class gas
state_class total_increasing
confidence mean digit confidence (0–1)

Submitting Meter Readings to Gas Networks Ireland

Gas Networks Ireland accept readings via their online form. submit_gni.py automates the submission. Your GPRN is on your gas bill; keep it in .env as GNI_GPRN rather than passing it on the command line.

# Dry run — scrape the form + print the payload, do NOT submit
python submit_gni.py --dry-run 10883

# Real submit (GPRN read from .env)
python submit_gni.py 10883.253   # decimal dropped → 10883
python submit_gni.py --force 10883  # ignore the local cooldown check

How the form works

The form is a Drupal webform that POSTs to https://www.gasnetworks.ie/gas-meters/meter-reading:

gprn=<your-gprn>&meter_reading=10883&op=Submit+a+reading&form_id=webform_submission_submit_meter_reading_node_1300_add_form&form_build_id=<one-time token>

Key things the script handles:

  • form_build_id changes on every page load and is tied to a session cookie. So submitting is a two-step round-trip: GET the page → scrape the fresh form_build_id out of the meter-reading form specifically (the page hosts three webforms) → POST it back reusing the same session cookies.
  • No CAPTCHA on the meter-reading form. The captcha_* / reCAPTCHA fields on that page belong to a separate "request support" form, not this one.
  • Whole m³ digits only. meter_reading has pattern="^\d+$" — the integer part of the reading, no decimal. The OCR produces 10883.253; the script submits 10883.
  • A browser User-Agent is required — default script UAs get a 403.

14-day cooldown

GNI only accepts one reading per GPRN every 14 days. Submitting too soon is rejected with: "Sorry, you have recently submitted a meter reading for GPRN number … on <date>. You will need to wait an additional 14 days …".

The script tracks submissions in gni_submissions.json (gitignored — it holds your GPRN) and:

  • Refuses before hitting the network if you're still inside the window.
  • Parses the date out of GNI's rejection message and records it, so local state stays in sync even with readings you submitted by hand.
  • --force bypasses the local check (GNI will still reject if it's genuinely too soon).

License

MIT

About

Read your gas meter automatically with a Raspberry Pi, a camera, and a YOLOv8 object detector — digit recognition, Home Assistant integration, and automatic submission to Gas Networks Ireland.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors