License Plate Recognition using Gemini 3.5 Flash API (Gate Opener & Notifications) with lights

:rocket: UPDATE: Added Visual Feedback Feature (UX Status Light)

I have just updated the project to include a Visual Feedback Light feature! When using cloud APIs, there's always a small delay (a few seconds) while the system processes the image. To eliminate the "waiting anxiety" of not knowing if the automation is running, you can now assign a status light (e.g., a pilot LED connected to a Shelly relay) to act as a real-time indicator.

How the light behaves now:

  • Steady Light: Vehicle detected! (Meaning: "I saw you, I'm taking the snapshot and processing the plate, please wait...")

  • Flashing Light (5 times): Access granted! (Meaning: "Success! Plate recognized, the gate is opening now.")

  • Light Turns Off Immediately: Unauthorized vehicle or failed read.

:gear: What changed?

  • The Python Script: Unchanged (No edits needed).

  • The Shell Command: Unchanged (No edits needed).

  • The Blueprint: Updated! I added a new required input field: Status Light (Shelly).

If you have already imported the blueprint, simply go to your Home Assistant Blueprints dashboard, click the three dots next to this blueprint, and select "Re-download blueprint" to get the latest version with this visual feedback feature included.Hello everyone!

I want to share a project I created to detect vehicle license plates using a Home Assistant camera through the Gemini 3.5 Flash API. It is extremely fast, highly intelligent (reads plates perfectly even at difficult angles), and does not require heavy local add-ons. When a recognized car arrives, it automatically triggers the gate switch and sends personalized notifications (TTS via Alexa and a pop-up alert on an LG webOS TV).

Since this setup requires a small helper script, installation is done in 3 simple steps:

1. The Python Script

Create a file at /config/www/tmp/analisar_matricula.py and paste the following code (don't forget to insert your own Gemini API Key in the configurations section):

Python

import sys
import requests
import base64

# ==============================================================================
# CONFIGURATIONS (The user must change these details)
# ==============================================================================
API_KEY = "PASTE_YOUR_GEMINI_API_KEY_HERE"
MODELO = "gemini-3.5-flash"
IMAGE_PATH = "/config/www/tmp/check_plate.jpg"
# ==============================================================================

def encode_image(image_path):
    with open(image_path, "rb") as image_file:
        return base64.b64encode(image_file.read()).decode('utf-8')

def analise_matricula():
    try:
        base64_image = encode_image(IMAGE_PATH)
    except Exception as e:
        # If the image doesn't exist at the moment, warn and exit safely
        print(f"Error: Image not found at {IMAGE_PATH}")
        sys.exit(1)

    url = f"https://generativelanguage.googleapis.com/v1beta/models/{MODELO}:generateContent?key={API_KEY}"

    headers = {
        "Content-Type": "application/json"
    }

    payload = {
        "contents": [{
            "parts": [
                {
                    "text": "Read the license plate in this image. Respond ONLY with the license plate text (e.g., AA00BB). If there is no license plate, respond 'NADA'."
                },
                {
                    "inline_data": {
                        "mime_type": "image/jpeg",
                        "data": base64_image
                    }
                }
            ]
        }]
    }

    try:
        response = requests.post(url, headers=headers, json=payload, timeout=8)
        
        if response.status_code == 200:
            resultado = response.json()
            # Safe extraction of the license plate text returned by Gemini
            texto = resultado['candidates'][0]['content']['parts'][0]['text']
            print(texto.strip())
        else:
            # Show the error if the Gemini API fails
            print(f"API Error: {response.status_code}")
            
    except Exception as e:
        print(f"Connection error")

if __name__ == "__main__":
    analise_matricula()

2. The Shell Command

Add the following lines to your configuration.yaml file, then go to Developer Tools -> YAML and Reload Shell Commands (or restart Home Assistant):

YAML

# Command that executes the license plate reading Python script
shell_command:
  analisar_matricula: "python3 /config/www/tmp/analisar_matricula.py"

3. The Blueprint

You can import the Blueprint directly by copying the URL below and adding it to your Blueprints dashboard:

  • Import Link: https://gist.githubusercontent.com/Rveiga86/cbe638cbfa9f7987af72f59b1aedb3ad/raw/d05766668dbe4c74d45fe3850fa465870f1dcb1e/gemini_matriculas.yaml

Once imported, simply create a new automation from this Blueprint and fill in the required fields (Sensors, Gate Switch, License Plates, and Car Names) directly through the Visual User Interface (UI). No further YAML editing required!

I hope you find this useful. Let me know if you have any questions!

why a py script and not an ai_task call?

AI Task - Home Assistant

I don't see why the script is required?

Great question! I went with a standalone Python script via shell_command instead of native conversation/AI actions for a few practical reasons:

​Zero Integration Overhead: It doesn’t require setting up a full Conversation Agent configuration or installing third-party custom components (like LLM Vision). It keeps the Home Assistant setup clean and lightweight.

​Instant & Clean stdout Parsing: The script explicitly instructs the Gemini API to return only the raw license plate string (e.g., 17BX72) and prints it directly to stdout. This allows the Home Assistant automation to capture it flawlessly in a response_variable and evaluate it with a simple Jinja template, without having to parse complex nested JSON structures from generic chat/conversation history objects.

​Speed & Latency: Bypassing the native Home Assistant intent/conversation pipeline shaves off valuable milliseconds. Since this automation controls a gate opener, every second counts when you are sitting in your car waiting.

​Granular Control: It allows me to easily manage the API payload, set a strict connection timeout (8 seconds), and handle errors safely in a couple of lines of code without relying on core integration updates.

​Hope this clears up why I chose this path!