Automation

BlackVueSync + Home Assistant: Auto-Pull BlackVue Dashcam Footage

Independent technologist · 200+ HA devices · GriswoldLabs
Updated July 8, 2026 16 min read

I run BlackVue dashcams in both household vehicles — a DR970X-2CH Plus II in one (front + rear) and a DR900X Plus in the other (front only). They both record continuously on a 256 GB microSD, but pulling footage off the SD card means physically taking the card out, plugging it into a laptop, and copying files. I’d done that exactly twice before I got tired of it and built a system that pulls footage automatically the moment a car drives into WiFi range of the house.

The whole thing is one Docker container, two HA ping sensors, and a 121-line package file. This post is the architecture, the YAML, and the gotchas.

The architecture

Camera WiFi (built-in)  →  Home Network

BlackVueSync container (Docker on Unraid)

/mnt/user/dashcam/
├── car/
└── truck/

Home Assistant ping sensors  →  binary_sensor.dashcam_*_online

Notifications + sync-trigger logic

Each BlackVue camera runs a small HTTP server when it’s powered on. The endpoints aren’t documented by BlackVue but they’re well-known in the community:

  • GET http://<camera-ip>/blackvue_vod.cgi — file list
  • GET http://<camera-ip>/blackvue_record/<path> — download a recording
  • No authentication

When the car is parked in the driveway and the camera is powered, it joins the home WiFi (because I configured it to) and serves these endpoints to anything on the LAN that asks. BlackVueSync is an open-source Docker container that polls those endpoints, downloads new recordings, and stores them locally. HA’s job is just to know whether each camera is currently reachable and to surface notifications when a sync starts or fails.

Power and parking mode: why the camera is even reachable

None of this works unless the camera stays powered while the car is parked. Left on the accessory circuit alone, a BlackVue shuts down shortly after you switch the ignition off — so for 24/7 recording, and to keep the Wi-Fi HTTP endpoints alive in the driveway, the cameras stay powered from a hardwire kit — mine runs off shore power (a garage GFCI outlet) rather than a tap into the car’s fuse box, which is exactly why a tripped GFCI once knocked a camera offline. A dedicated battery pack like a Cellink NEO or BlackVue’s own B-124 does the same job without draining the starter battery. Parking mode is what keeps them recording motion and impacts while the car sits there.

The practical upshot for this build: a BlackVue only serves blackvue_vod.cgi and the other endpoints while it’s powered and joined to home Wi-Fi. That’s exactly the window we want — car home, ignition off, camera in parking mode, footage waiting to be pulled.

Why this beats the BlackVue cloud

BlackVue sells a cloud subscription. It’s fine. It uploads from the camera over LTE / WiFi, stores footage in their cloud, lets you view it from your phone. Two reasons I don’t use it:

  1. The cameras are recording 24/7 and uploading every clip would be expensive bandwidth-wise. The cars together generate 30-60 GB/day of footage. On metered cellular that’s prohibitive. The cameras have a setting to upload only event-triggered clips, but I’d rather have everything archived locally.
  2. The footage I actually care about is the long-form continuous recording, not just events. When something happens — a parking-lot ding, a road incident — I usually want the 10 minutes of context around it, not just the 30-second event clip BlackVue’s algorithm decides to upload.

Local sync to a NAS gets me both. Footage stays on the cameras for as long as the SD card holds it (rolling buffer), and a permanent archive lives on the NAS with whatever retention I want.

The Docker setup: running BlackVueSync

acolomba/blackvuesync is the image I’m using. It’s MIT-licensed, well-maintained, and handles the boring parts: parallel downloads, retry on disconnect, disk-full safety, day-folder grouping. A minimal docker-compose.yml looks like:

services:
  blackvuesync-car:
    image: acolomba/blackvuesync:latest
    container_name: blackvuesync-car
    restart: unless-stopped
    environment:
      - ADDRESS=<car-camera-ip>
      - KEEP=30d
      - GROUPING=daily
      - CRON=1              # cron-friendly logging; sync runs on a fixed 15-min interval
      - PUID=99            # NAS user UID (99 = 'nobody' on Unraid)
      - PGID=100           # NAS group GID (100 = 'users' on Unraid)
      - TZ=America/New_York
    volumes:
      - /mnt/user/dashcam/car:/recordings
    networks:
      - default

  blackvuesync-truck:
    image: acolomba/blackvuesync:latest
    container_name: blackvuesync-truck
    restart: unless-stopped
    environment:
      - ADDRESS=<truck-camera-ip>
      - KEEP=30d
      - GROUPING=daily
      - CRON=1
      - PUID=99
      - PGID=100
      - TZ=America/New_York
    volumes:
      - /mnt/user/dashcam/truck:/recordings

PUID/PGID/TZ aren’t cosmetic — they’re the fix for the single most common BlackVueSync first-run complaint. Without them the container writes every file as root, and then your NAS user can’t move, rename, or delete the footage without sudo; retention deletes can fail for the same reason. Set them to the UID/GID of whatever account owns your share (on Unraid, nobody:users = 99:100), and TZ so the day-folder grouping and log timestamps land in your local time instead of UTC.

Two containers, one per camera. They poll their assigned camera every 15 minutes; if the camera responds, they pull whatever’s new. If the camera doesn’t respond (because the car isn’t home), they log a connection failure and try again next cycle. There’s no harm in polling a camera that isn’t there — BlackVueSync handles the connection refused gracefully and just waits.

KEEP=30d is the local retention. Anything older than 30 days in /mnt/user/dashcam/<vehicle>/ gets auto-deleted on each sync run. I’ve experimented with longer windows; 30 days is the sweet spot between “I might want to look at last month’s drive” and “this is eating disk faster than I want.”

GROUPING=daily puts each day’s footage in its own subfolder, which makes manual browsing dramatically easier. Without it, you get one giant flat directory of YYYYMMDD_HHMMSS_NF.mp4 files and good luck finding anything.

BlackVueSync options worth knowing

The four env vars above cover the common case, but the tool exposes a lot more. These are the ones worth knowing (check the acolomba/blackvuesync README for the current, authoritative list — options do get added):

OptionWhat it does
KEEPLocal retention window, e.g. 30d, 2w. Older files are pruned each run.
GROUPINGFolder layout: none, daily, weekly, monthly, yearly.
PRIORITYDownload order: date (oldest first), rdate (newest first), or type (by clip type — manual, then event, normal, parking).
MAX_USED_DISKStop downloading once the target disk hits this percent (default ~90%). Belt-and-suspenders against a full array.
INCLUDE / EXCLUDEFilter which recording types download — normal vs. event vs. parking, front vs. rear. This is the lever for a “pull only event clips” archive.
SKIP_METADATADon’t fetch the per-clip accelerometer/GPS sidecar files if you only want the video.
TIMEOUTPer-request timeout, in seconds — bump it on a weak Wi-Fi link.
DRY_RUNList what would download without writing anything. Your best friend on first setup.
VERBOSE / QUIETLog level. VERBOSE when debugging, QUIET for quiet cron logs.
PUID / PGID / TZFile ownership and timezone, covered above.

If you only care about incidents, EXCLUDE the normal (N) recordings and keep the event (E) ones — that maps directly to the _EF/_ER suffixes described later in this post.

Which BlackVue models (and firmware) this works with

I only run two cameras myself — the DR970X-2CH Plus II and the DR900X Plus — so treat the table below as BlackVue’s published specs plus BlackVueSync’s compatibility notes, not something I’ve personally bench-tested on every model. The rule of thumb: if the model has built-in Wi-Fi and exposes the local HTTP API, BlackVueSync can pull from it.

ModelFront resolutionWi-FiDual-channelLocal sync
DR970X (Plus / 2CH)4K UHDDual-bandYesYes
DR900X (Plus)4K UHDDual-bandYesYes
DR750X (Plus)Full HD (1080p)Dual-bandYesYes
DR590XFull HD (1080p)2.4 GHzYesYes

One firmware caveat: the local download API that BlackVueSync relies on is documented as available from firmware V1.009 onward. If curl reaches the camera but nothing downloads, an out-of-date firmware is a prime suspect — update from the BlackVue app or by copying the firmware onto the SD card. The accelerometer and GPS metadata format also shifts between firmware versions, which is another reason to stay current if you plan to parse the sidecar data.

Static IPs are not optional

The camera IP has to be stable. If it changes — say your DHCP lease rolls over — the Docker container starts pinging a black hole and you stop getting footage. I assign DHCP reservations in UniFi for both cameras based on MAC, and the reservations live alongside the rest of my smart-home reservations.

You could put the IPs in HA secrets.yaml and template them into the docker-compose.yml at deploy time, but I haven’t bothered — the IPs are stable enough that hardcoding them in docker-compose.yml is fine.

The HA side: ping sensors + sync notifications

HA isn’t doing the actual sync — that’s all BlackVueSync’s job. HA’s role is to surface visibility: tell me when a car comes home, when a camera disconnects unexpectedly, and let me toggle notifications during a road trip when I don’t care about every sync event.

Two ping integrations, added through the UI (Settings → Devices → Add Integration → Ping):

  • Dashcam Car Online — pings the car camera IP every 30 seconds
  • Dashcam Truck Online — pings the truck camera IP every 30 seconds

Why ping and not device_tracker? A lot of BlackVue-on-HA guides trigger the sync off device_tracker (an nmap or router-based presence scan, away→home). That works, but it answers “is a device on the network,” whereas a ping sensor answers the question this system actually depends on: “is this specific camera replying right now.” It’s the more direct signal, and it doubles as the disconnect alarm below. If you already run device_tracker presence for people, either approach is fine.

These produce binary_sensor.dashcam_car_online and binary_sensor.dashcam_truck_online. The package file uses these as the foundation:

input_boolean:
  dashcam_sync_notifications:
    name: Dashcam Sync Notifications
    icon: mdi:dashcam

automation:
  - id: dashcam_car_online
    alias: "Dashcam Car Online"
    description: "Notify when car dashcam connects to WiFi (car arrived home)"
    triggers:
      - trigger: state
        entity_id: binary_sensor.dashcam_car_online
        from: "off"
        to: "on"
        for:
          seconds: 30
    conditions:
      - condition: state
        entity_id: input_boolean.dashcam_sync_notifications
        state: "on"
    actions:
      - action: notify.charles
        data:
          title: "Dashcam Sync"
          message: "car dashcam connected — footage syncing to NAS"
          data:
            tag: dashcam-car-sync
    mode: single

The for: 30 seconds debounces — without it, brief WiFi handshake flickers when the car first parks send the binary sensor through three or four state changes in a row, and you’d get redundant notifications.

The tag: dashcam-car-sync is a notification tag. On Android (and iOS with the companion app), notifications with the same tag replace each other instead of stacking. So if you arrive home, leave, arrive again twenty minutes later, you don’t get six “Dashcam Sync” notifications piling up in your tray — you get one, updated in place.

input_boolean.dashcam_sync_notifications is a manual on/off toggle. I have a card for it on my home dashboard. When I’m road-tripping and the camera is bouncing on/off WiFi at gas stations and hotels, I flip it off so my phone isn’t pinging.

The disconnect alert

The other half is detecting an unexpected disconnect — the camera was online, I’m still at home, and now the camera has gone offline. That usually means the camera or its WiFi has died and I want to know:

- id: dashcam_car_disconnect_alert
  alias: "Dashcam Car Disconnect Alert"
  description: "Alert if car dashcam drops off WiFi while car should be home"
  triggers:
    - trigger: state
      entity_id: binary_sensor.dashcam_car_online
      from: "on"
      to: "off"
      for:
        minutes: 10
  conditions:
    - condition: state
      entity_id: person.charles
      state: "home"
    - condition: state
      entity_id: input_boolean.dashcam_sync_notifications
      state: "on"
  actions:
    - action: notify.charles
      data:
        title: "Dashcam Offline"
        message: "car dashcam lost WiFi connection (you're still home)"
        data:
          tag: dashcam-car-offline

Two key pieces:

  1. for: 10 minutes prevents this firing when I drive away. The car leaves the driveway, the camera goes off WiFi, ten minutes later the automation would fire — but by then I’m not home, so the person.charles: home condition fails. No alert.
  2. person.charles: home is the condition that says “this only fires if the car is supposed to be parked here.” Without it, every legitimate departure would generate a “dashcam offline” alert.

I’ve gotten this notification twice in a year. Once was a tripped GFCI on the garage outlet that powers the car’s hardwire kit; once was a flaky WiFi extender. Both real problems worth knowing about. Zero false positives.

File naming, in case you’re parsing it

BlackVue’s filename convention:

  • YYYYMMDD_HHMMSS_NF.mp4 — Normal recording, Front camera
  • ..._NR.mp4 — Normal, Rear (DR970X-2CH only)
  • ..._EF.mp4 — Event-triggered, Front (sudden brake, impact, etc.)
  • ..._ER.mp4 — Event-triggered, Rear
  • ..._PF.mp4 — Parking mode, Front (motion-triggered while parked)
  • ..._PR.mp4 — Parking mode, Rear

If you’re scripting against this — say, automatically pulling out only event clips for a separate “interesting events” archive — the suffix is the only signal you need.

What this doesn’t do

A few things this setup deliberately doesn’t include, in case you’re expecting them:

  • Cloud uploads. I don’t push footage anywhere offsite. If I cared, I’d add a separate rclone job that watches /mnt/user/dashcam/ and pushes to B2 or similar. Haven’t bothered.
  • Event detection / parsing. The cameras flag their own events with the _EF/_ER suffix, but I don’t have anything that watches the directory for new event clips and notifies me. The notification fires when the camera comes online, not when an event is recorded. I’ve thought about a folder-watcher that fires a “new event clip arrived” notification, but in practice I check the archive when something happened, not the other way around.
  • OCR or video analysis. I’ve experimented with running each new clip through a license-plate OCR for “log every plate that drives past my house” — works fine, requires GPU time, and turns out to be more interesting in concept than useful in practice. Maybe a separate post.

If you’re building this from scratch

The order I’d build in:

  1. Get the cameras on home WiFi with static IPs. This is the part most people skip and then wonder why nothing works. Static IPs are not optional.

  2. Stand up acolomba/blackvuesync for one camera and verify files appear in your mount. Don’t write a single line of HA YAML until this works manually. Do this in two cheap steps before you commit to a compose file. First, with the car parked and the camera on Wi-Fi, confirm the endpoint actually answers:

    curl -v http://<camera-ip>/blackvue_vod.cgi

    You should get back a list of recording paths, not a connection refused. Then do a throwaway one-shot run with DRY_RUN and RUN_ONCE on — so it makes a single pass and exits instead of starting the 15-minute sync loop — to preview what would download without writing anything:

    docker run --rm \
      -e ADDRESS=<camera-ip> \
      -e DRY_RUN=1 \
      -e VERBOSE=1 \
      -e RUN_ONCE=1 \
      -v /mnt/user/dashcam/car:/recordings \
      acolomba/blackvuesync:latest

    If curl succeeds and the dry run lists the files you expect, then wire up the real docker-compose.yml with KEEP and your other options — with RUN_ONCE unset, the container runs the sync loop on its own fixed 15-minute interval.

  3. Add ping sensors in HA and confirm they flip state when the car leaves and arrives. Again, no automations yet — just confirm the sensors are reliable.

  4. Then write the notification automations, with the for: debounces from the start.

Skipping straight to “I’ll write the HA YAML and figure out the rest as I go” is the fast path to debugging an automation that’s broken because the underlying ping sensor was always wrong.

The full package is about 120 lines once you include the disconnect alerts for both vehicles. Reasonable amount of YAML for a system that ends up running unattended for months.

Troubleshooting

Most of what goes wrong with a local BlackVue sync falls into a handful of buckets. Symptom → likely cause → fix:

SymptomLikely causeFix
connection refused / camera unreachableCar isn’t home, camera unpowered, or its IP changedConfirm the car is parked and the camera is on Wi-Fi; curl http://<ip>/blackvue_vod.cgi; if the IP drifted, set a DHCP reservation.
Files owned by root, can’t move/deleteContainer writing as rootSet PUID/PGID to your NAS user; chown the existing files once to clean up.
curl returns the file list but nothing downloadsFirmware too old for the download API, or the mount isn’t writableUpdate camera firmware to V1.009+; check the /recordings volume is writable by PUID.
Disk keeps filling upKEEP too long, or MAX_USED_DISK not setShorten KEEP; confirm MAX_USED_DISK (default ~90%) so it stops before the array is full.
Sync works for days, then stopsDHCP lease rolled the camera to a new IPDHCP reservation / static IP — the single most common “it just stopped” cause.
Container logs “another instance running”A crashed run left a stale lock file in the destinationStop the container, remove the lock file in the recordings folder, and make sure only one container maps each camera/mount.

If you’re stuck, run the container with VERBOSE=1 and DRY_RUN=1 again — the verbose log almost always names the actual failure (auth-free endpoint 404, disk threshold hit, permission denied on write) instead of leaving you guessing.

Bonus: a live MJPEG feed in Home Assistant

This build is archival — it pulls recorded files, it doesn’t show a live picture. But the same powered-and-on-Wi-Fi camera also exposes a live stream at http://<camera-ip>/blackvue_live.cgi, and if you want a driveway view on your HA dashboard, you can add it as an MJPEG camera. I don’t run this myself, so treat it as an optional add-on rather than part of the system above:

camera:
  - platform: mjpeg
    name: Driveway Dashcam
    mjpeg_url: http://<camera-ip>/blackvue_live.cgi

Two caveats: it only works while the car is home and powered (the entity goes unavailable the moment you drive off, which is expected), and a continuous live pull works the camera’s Wi-Fi harder than the periodic file sync does. As a “glance at the driveway while I’m home” card, though, it’s a nice free extra on top of the archival pipeline.


Related guides: IoT VLAN segmentation with UniFi · Storage-mode to YAML dashboards · Monitoring the smart home with Uptime Kuma

Tags #home-assistant #blackvue #docker #dashcam #nas
Share X / Twitter Facebook
Keep reading