Skip to content

Home Assistant

Daniel Frenkel edited this page Jul 11, 2026 · 2 revisions

Home Assistant Integration

By default a MiniSpeedCam uploads every pass to your account at minispeedcam.com. Instead, you can point a device at a local Home Assistant webhook, so each capture — speed, photo, and the per-pass telemetry — arrives in HA as an automation trigger you can turn into sensors, dashboards, notifications, and automations.

This works because the device's data destination is user-editable. The Wifi Settings → Data Server field lets you replace the cloud URL with any endpoint, and the firmware automatically uses plain HTTP (not HTTPS) when the URL starts with http://, which is exactly what a LAN Home Assistant speaks.

⚠️ Re-pointing the Data Server replaces the cloud, it doesn't add to it. A device sends to one destination. While it's pointed at Home Assistant, your captures go to HA only — they will not appear on minispeedcam.com. To go back, clear the field (see Reverting to the cloud).


Requirements

  • Firmware with the Data Server field. Open the portal → Wifi Settings tab. If you don't see an API Base URL box, update first — see Firmware Update.
  • The device and Home Assistant on the same local network. The webhook is reached over plain HTTP on your LAN; it is not encrypted, so keep it to a trusted home network.
  • Home Assistant reachable at a stable address — ideally a fixed/reserved IP for your HA server (a DHCP lease change would break the URL). The default HA port is 8123.

How it works

The device builds its upload URL as <API Base URL> + /capture. So if you set the base to your HA webhook root, the capture POST lands on a webhook whose ID is capture:

API Base URL:   http://192.168.1.50:8123/api/webhook
capture POST →  http://192.168.1.50:8123/api/webhook/capture

You create a Home Assistant webhook automation (or a webhook-triggered template sensor) listening on webhook_id: capture, and HA receives the JSON body of every pass.

The device also sends two housekeeping POSTs to the same base — …/register_device at boot and …/camera as a periodic health heartbeat. With only the capture webhook defined, HA logs a harmless "Received message for unregistered webhook" for those. You can ignore them, or capture the heartbeat too — see Optional: device health.


Step 1 — Create the capture webhook in Home Assistant

You can do this entirely in YAML (paste into configuration.yaml or a package). Choose one of the two patterns below — both register the same capture webhook, so you can't use both at once.

🔑 The webhook ID must be exactly capture — that's the suffix the firmware appends. You choose the rest of the URL (host/port/path) via the Data Server field, but not this last segment.

Pattern A — Speed + telemetry only (simplest)

A trigger-based template sensor that turns each pass into a sensor with attributes. No photo. Paste into configuration.yaml and restart HA:

template:
  - trigger:
      - platform: webhook
        webhook_id: capture
        allowed_methods: [POST]
        local_only: true          # only accept from the local network
    sensor:
      - name: "MiniSpeedCam Last Speed"
        unique_id: minispeedcam_last_speed
        state: "{{ trigger.json.speed_actual }}"
        unit_of_measurement: "mph"   # change to "km/h" if the device is set to KPH
        state_class: measurement
        attributes:
          direction: >-
            {{ {'0':'unknown','1':'approaching','2':'receding'}[trigger.json.direction] }}
          peak_snr: "{{ trigger.json.peak_snr }}"
          mean_speed: "{{ trigger.json.mean_speed }}"
          frame_count: "{{ trigger.json.frame_count }}"
          duration_ms: "{{ trigger.json.duration_ms }}"
          has_photo: "{{ trigger.json.send_photo }}"
          captured_at: "{{ now().isoformat() }}"

sensor.minispeedcam_last_speed now holds the most recent speed, and its attributes carry the rest of the pass data. Good enough if you just want the numbers in HA.

Pattern B — Speed + saved photo (full)

Since a webhook can have only one owner, do everything from a single automation: save the JPEG with a shell_command, then re-broadcast the data as an event so a template sensor can read it. This replaces Pattern A.

1. Decode helper — add a shell_command that base64-decodes the photo to a file:

shell_command:
  minispeedcam_save_photo: >-
    mkdir -p /config/www/speedcam &&
    printf '%s' '{{ photo_b64 }}' | base64 -d > '/config/www/speedcam/{{ stamp }}.jpg' &&
    cp '/config/www/speedcam/{{ stamp }}.jpg' /config/www/speedcam/latest.jpg

2. Webhook automation — owns the capture webhook; saves the photo and fires an event:

automation:
  - alias: "MiniSpeedCam capture"
    trigger:
      - platform: webhook
        webhook_id: capture
        allowed_methods: [POST]
        local_only: true
    action:
      # Only passes that exceeded Photo Speed carry an image (send_photo == "true").
      - choose:
          - conditions: "{{ trigger.json.send_photo == 'true' }}"
            sequence:
              - service: shell_command.minispeedcam_save_photo
                data:
                  photo_b64: "{{ trigger.json.photo.contents }}"
                  stamp: "{{ now().strftime('%Y%m%d-%H%M%S') }}"
      # Re-broadcast the pass so the template sensor below can pick it up.
      - event: minispeedcam_capture
        event_data:
          speed: "{{ trigger.json.speed_actual }}"
          direction: "{{ trigger.json.direction }}"
          peak_snr: "{{ trigger.json.peak_snr }}"
          mean_speed: "{{ trigger.json.mean_speed }}"
          frame_count: "{{ trigger.json.frame_count }}"
          duration_ms: "{{ trigger.json.duration_ms }}"
          has_photo: "{{ trigger.json.send_photo }}"

3. Speed sensor — a template sensor driven by that event (no webhook conflict):

template:
  - trigger:
      - platform: event
        event_type: minispeedcam_capture
    sensor:
      - name: "MiniSpeedCam Last Speed"
        unique_id: minispeedcam_last_speed
        state: "{{ trigger.event.data.speed }}"
        unit_of_measurement: "mph"   # change to "km/h" if the device is set to KPH
        state_class: measurement
        attributes:
          direction: >-
            {{ {'0':'unknown','1':'approaching','2':'receding'}[trigger.event.data.direction] }}
          peak_snr: "{{ trigger.event.data.peak_snr }}"
          mean_speed: "{{ trigger.event.data.mean_speed }}"
          frame_count: "{{ trigger.event.data.frame_count }}"
          duration_ms: "{{ trigger.event.data.duration_ms }}"
          has_photo: "{{ trigger.event.data.has_photo }}"
          captured_at: "{{ now().isoformat() }}"

4. Show the latest photo — a local_file camera pointed at the file the helper writes:

camera:
  - platform: local_file
    name: "MiniSpeedCam Latest"
    file_path: /config/www/speedcam/latest.jpg

Each pass overwrites latest.jpg (live view) and keeps a timestamped copy in /config/www/speedcam/ for history. Because they're under www/, you can also open any of them in a browser at http://<ha>:8123/local/speedcam/latest.jpg.

📸 Photo notes. base64 and printf are present on Home Assistant OS / Container installs; a stripped-down Core/venv install may need GNU coreutils. If the camera entity errors with a path warning, add the folder to homeassistant: allowlist_external_dirs: ["/config/www/speedcam"]. Only passes whose top speed reached Photo Speed include an image; slower passes are speed-only.


Step 2 — Point the device at Home Assistant

  1. Open the device portal and go to the Wifi Settings tab.

  2. In API Base URL (blank = default), enter your HA webhook root — host and port, no trailing /capture:

    http://192.168.1.50:8123/api/webhook
    

    (Use your HA server's actual IP and port.)

  3. Click Save Settings. The device reboots and starts sending captures to HA.

The device prints the active base on its USB serial console at boot ([API] base URL: http://…), which is a quick way to confirm what it's using.


Step 3 — Verify

  1. Trigger a pass (walk a vehicle — or yourself fast enough — past the unit).
  2. In Home Assistant:
    • Pattern A: open Developer Tools → States and check sensor.minispeedcam_last_speed.
    • Pattern B: the same sensor updates, and camera.minispeedcam_latest shows the photo (for passes over Photo Speed).
  3. Still nothing? See Troubleshooting.

Webhook payload reference

Every capture POST is JSON (Content-Type: application/json). Fields available as trigger.json.<field>:

Field Meaning
speed_actual Top speed for the pass, in the device's units (MPH or KPH per the MPH/KPH setting).
send_photo "true" if a photo is attached, "false" for a speed-only pass.
photo.filename image.jpg (present only when send_photo is "true").
photo.contents The JPEG, base64-encoded (present only when send_photo is "true").
direction 0 = unknown, 1 = approaching (front plate), 2 = receding (rear plate).
mag_trend Echo-energy centroid 0–100 (50 = symmetric pass; 255 = too few echoes).
peak_mag Strongest radar echo magnitude in the pass (closest approach).
peak_snr Strongest detection SNR (decimal) — detection confidence.
mean_speed Mean of the valid speed samples (decimal) — steady vs. braking shape.
frame_count Number of valid radar samples that made up the pass.
duration_ms Wall-clock length of the pass, in milliseconds.
device_token The unit's identity secret. Useful as a per-device key if you point several units at one HA.

💡 Multiple devices → one HA. They all POST to the same capture webhook. Branch on trigger.json.device_token (or give each unit its own HA via separate base URLs) to tell them apart.


Optional: device health (heartbeat)

The periodic …/camera heartbeat carries a device-health snapshot. To capture it, add a second webhook with ID camera (same pattern as above). Its fields:

Field Meaning
rssi WiFi signal strength (dBm).
free_heap / min_free_heap Current and lifetime-low free heap (bytes).
uptime_s Seconds since boot.
boot_count / reset_reason Reboot count and last reset cause.
esp_fw / stm_fw Installed ESP32 and STM32 firmware versions.
reject_speed / reject_proximity Lifetime counts of discarded readings (corrupt replies / distant traffic).
ip_address The device's current LAN IP.

These make handy diagnostic sensors (signal, uptime, firmware version on a dashboard).


Reverting to the cloud

To send captures back to minispeedcam.com:

  1. Portal → Wifi SettingsAPI Base URL.
  2. Clear the field (leave it blank) and click Save Settings.

A blank value restores the built-in default server on reboot. (You can also type a full URL to point at a specific environment.)


Troubleshooting

Symptom Likely cause / fix
No webhook ever fires Confirm the device and HA are on the same LAN, and the base URL is exactly http://<ha-ip>:8123/api/webhook with no /capture and no trailing slash. Check the device serial log line [API] base URL: ….
HA log: "Received message for unregistered webhook" for register_device / camera Normal — the device also sends boot-registration and heartbeat POSTs. Ignore them, or define those webhooks (see device health).
Connection refused / nothing arrives, but URL looks right The device only uses plain HTTP when the base starts with http://. An https:// base triggers a TLS handshake your local HA won't answer. For local use, keep it http://.
Speed value looks doubled/halved The sensor's unit_of_measurement doesn't match the device's MPH/KPH setting. The number is whatever the device measures; set the unit to match (see Device Settings → MPH/KPH).
direction is unknown a lot Direction needs enough echoes / proximity tuning. It's best with the proximity thresholds set — see Device Settings → Radar signal (proximity).
Photo never saves (Pattern B) Check base64/printf exist in your HA environment, that /config/www/speedcam is writable, and add it to allowlist_external_dirs if the camera entity warns about the path. Remember only passes over Photo Speed include an image.
Captures stopped appearing on minispeedcam.com Expected — a device sends to one destination. Revert to the cloud to restore cloud uploads.
Portal has no API Base URL field Your firmware predates this feature. Update via Firmware Update.