If you have a car in Romania, you care about three things: the vignette (rovinietă), RCA (liability insurance), and ITP (periodic technical inspection). The vignette is easy to check—erovinieta has a public API with no captcha. RCA and ITP live on other sites (BAAR and RAROM), and both hide the result behind a captcha. I wanted all of this in Home Assistant: one place to see whether each car is up to date, and create alerts for when things expire.
So I did two things. First, I built a small private API that drives a browser to BAAR and RAROM, solves the captchas, and returns structured data. I run it for my own use and don’t plan to publish it. Second, I published a Home Assistant integration that talks to the official vignette API and, optionally, to any RCA/ITP API you provide. That way you get vignette out of the box, and if you build your own scraper (or use someone else’s), you can plug it in.
What the integration does
The integration is called RO Auto and is installable via HACS. You add one or more cars (name, make, model, year, VIN, registration number). For each car it creates sensors:
- Vignette: valid/invalid and expiry date (from the public erovinieta API).
- RCA: optional. Only if you enable it and configure an RCA API URL plus Basic auth. Then you get RCA valid/not and validity start/end dates.
- ITP: optional. Same idea, enable ITP, set an ITP API URL and Basic auth, and you get ITP status and expiry.
Vignette works with zero extra setup. RCA and ITP are off until you point the integration at an API that can actually query BAAR and RAROM, because those sites don’t offer a public API.
Why a separate API for RCA and ITP
BAAR (RCA) and RAROM (ITP) both use captchas. BAAR uses reCAPTCHA; RAROM uses a custom image captcha (red digits on a dotted background). You can’t call them from a simple HTTP client. So the flow is: run a service that uses a real browser (or a headless one), solve the captcha, scrape or intercept the result, and expose that over HTTP. Home Assistant then talks only to your API, with a stable contract (RCA: plate in, validity out; ITP: VIN in, validity out). The integration doesn’t care how you implement that API—only that it responds with the right JSON and that you protect it (e.g. Basic auth) so only your HA instance can call it.
Libraries used
The most important ones in the API and scrapers:
- DrissionPage – browser automation (Chromium). Used for both BAAR and RAROM: open the page, fill forms, click, run JS in the page (e.g. to grab the captcha image), and read the DOM. Lighter than a full Selenium setup and plays well with a single browser per request.
- FastAPI – the HTTP API. Async endpoints,
Pydanticfor request/response models, andrun_in_threadpoolto call the blocking browser code without blocking the event loop. - OpenCV (
opencv-python-headless) – for the RAROM captcha: image preprocessing (HSV mask, morphology), digit segmentation, and template matching. Headless build so it runs in Docker without a display. - pydub and SpeechRecognition – reCAPTCHA audio path. Download the challenge audio, convert to a format the recognizer accepts (e.g. with
pydub), then pass it toSpeechRecognitionto get the text and type it back into the form.
Pydantic comes with FastAPI for the request bodies. Pillow and NumPy are used for image handling and array ops alongside OpenCV.Contract the integration expects
If you enable RCA, the integration will POST to your base URL +
/rca/check with Basic auth. Body:{ "plate": "B123ABC" }It expects a JSON object with at least:
query_date– when the check was done (e.g. ISO or "DD.MM.YYYY").is_valid– boolean.validity_start_date– string, e.g. "DD.MM.YYYY".validity_end_date– string, e.g. "DD.MM.YYYY".
For ITP, it POSTs to
/itp/check with:{ "vin": "WVWZZZ3CZWE123456" }Expected response shape:
status– e.g."ok"when the page indicated a valid ITP.itp_valid_until_raw– string (date or normalized ISO); the integration uses this for the expiry sensor.attempts– optional, useful for debugging.result_vin– optional, for sanity checks.
The integration maps these into sensor attributes (e.g.
rcaIsValid, rcaValidityEndDate, itpIsValid, itpValidUntilRaw). As long as your API returns something compatible, you’re good.Implementing the RCA side (BAAR)
BAAR’s site uses a form: you choose “by plate”, enter the plate, accept the privacy checkbox, solve reCAPTCHA, and submit. The result page shows whether the car has valid RCA and the validity interval. So your API needs to:
- Drive a browser to the BAAR URL.
- Select “by plate” and fill the plate (normalized: no spaces, uppercase).
- Solve reCAPTCHA. An audio-based solver works: click the checkbox, get the audio challenge, download the audio, run speech-to-text, type the answer. There are existing solvers you can plug into a Chromium automation library; the usual caveat applies: don’t hammer the site or you may get your IP flagged.
- Submit and wait for the result block.
- Parse the result (e.g. “are o polita rca valida” vs “nu are”) and extract validity dates from the page or from an image if they’re shown in a picture (e.g. OCR the validity area).
I run this in a thread pool so the FastAPI app stays async; 3 concurrent requests at a time. Each run uses a fresh temp directory for the browser profile so there’s no cross-request lock.
Example endpoint shape:
@app.post("/rca/check")
async def rca_check(req: CheckRequest):
plate = normalize_plate(req.plate)
if not plate:
raise HTTPException(status_code=400, detail="plate is required")
async with _sema: # limit concurrency
result = await run_in_threadpool(check_baar_rca, plate)
return resultcheck_baar_rca is the synchronous function that builds the browser, fills the form, solves the captcha, and returns a dict with query_date, is_valid, validity_start_date, validity_end_date. If the site is down or the captcha fails, raise or return a 500 so the integration can retry or show an error.Implementing the ITP side (RAROM)
RAROM’s ITP check is a form with VIN and a custom image captcha: a few digits (e.g. four) on a red dotted background. reCAPTCHA solvers don’t help here. I use template matching with
OpenCV: segment the digits from the captcha image, then match each digit region against a small set of templates (one per digit 0–9). The templates are just small images you create once per digit.Flow:
- Open the RAROM ITP page, fill the VIN.
- Grab the captcha image from the page (e.g. draw the img element to a canvas and export as PNG so you get the exact pixels the user would see).
- Preprocess the image (e.g. red mask in HSV, clean noise with morphology) to get a binary mask of the digits.
- Find connected components or bounding boxes for each digit, crop to a fixed size, then correlate each crop with your templates. Pick the digit with the best score per position and concatenate into a string.
- Fill the verification field and submit. If the page says “incorrect code”, the captcha has changed; retry with a fresh captcha.
Template bootstrap: the first time you have no templates. Run the script in a “generate only” mode: solve the captcha (or guess), don’t submit, and export each digit crop with a consistent size into a folder like
rarom_templates/0/, rarom_templates/1/, … Then run again with a few more times to get more captcha images and export more crops into _unlabeled, and move them into the right digit folders. After you have a handful of templates per digit, the matcher becomes reliable enough for production. Template matching is more robust than generic OCR for this kind of fixed-font, noisy captcha.Response shape for the integration: return something like
{"status": "ok", "itp_valid_until_raw": "2026-05-12", "attempts": 1, "result_vin": "..."} when the result page indicates valid ITP; otherwise adjust status and leave itp_valid_until_raw empty so the integration can mark the sensor appropriately.Exposing the API to Home Assistant
The integration uses Basic auth. You can run your FastAPI app behind a reverse proxy (I use Pangolin) and add Basic auth there, or add a middleware in the app that checks a header. Store username/password in the integration’s config (or in the options flow when you enable RCA/ITP). Don’t expose the API on the public internet without auth; the integration is designed to call a URL you control, from your HA instance.
If you run the scraper in Docker, use a small concurrency limit so you don’t spawn too many Chromium processes. A semaphore around the blocking “run browser + solve captcha” call keeps the API responsive and avoids overload.
Summary
- RO Auto on GitHub: HACS integration for Romanian vignette (works out of the box) and optional RCA/ITP if you provide your own API.
- Vignette comes from the public erovinieta API; no captcha, no extra service.
- RCA and ITP require a separate service that can solve BAAR’s reCAPTCHA and RAROM’s image captcha. The integration expects POST
/rca/check(plate → validity) and POST/itp/check(VIN → status + expiry), with Basic auth. - My own implementation is for personal use and isn’t public. But the contract is simple: implement those two endpoints, return the expected fields, and the integration will show vignette, RCA, and ITP in Home Assistant with one sensor per car and optional expiry dates.
- If you build your own RCA/ITP backend: use a dedicated browser profile per request (or per attempt), limit concurrency, and for RAROM consider template matching plus a one-time template bootstrap step instead of generic OCR. That should be enough to replicate the idea and plug it into RO Auto.


