A lightweight Go bridge that receives IVS events from a Dahua NVR over HTTP and publishes them to Home Assistant via MQTT discovery.
Each camera automatically gets four HA binary sensors: tripwire vehicle, tripwire human, intrusion vehicle, intrusion human.
- Single static binary — no runtime dependencies, no Python, no pip. Just
scpand run. - MQTT discovery — binary sensors auto-register in HA on startup. No manual YAML sensor definitions needed.
- Auto-off — the bridge publishes
OFFafter a configurable delay (off_delay), so HA dashboards show real-time detection state without manual reset. The timer resets on every incoming event, even if anti-dither suppresses it. - Anti-dither — suppress repeated MQTT publishes per camera/rule within a configurable time window to reduce noise, while still extending the OFF timer on every event.
- Last Will & Testament — HA marks sensors as
unavailableif the bridge crashes or loses connection. - IP allowlist — optionally restrict which IPs can send events (e.g., NVR VLAN only).
- Rotating logs — automatic log rotation (5 x 5 MB).
- Monitoring —
/healthand/statsendpoints for uptime and metrics. - Security hardened — ships with a locked-down systemd unit (read-only filesystem, no capabilities, restricted syscalls).
Dahua NVR ──HTTP POST──> dahua2mqtt ──MQTT──> Home Assistant
(Go binary) (binary_sensor.*)
- NVR sends an IVS event to
http://<bridge>:<port>/cgi-bin/NotifyEvent. - The bridge validates and extracts camera name, event type, and object class.
- If anti-dither passes, an MQTT message (
ON) is published to the matching sensor topic. Either way, the OFF timer resets. - After
off_delayseconds of inactivity, the bridge publishesOFF.
| Purpose | Topic |
|---|---|
| Availability (LWT) | dahua2mqtt/status (online / offline) |
| Sensor state | dahua2mqtt/{camera}/{tripwire|intrusion}/{vehicle|human} |
| HA discovery | homeassistant/binary_sensor/dahua2mqtt_{camera}_{type}_{object}/config |
- Go 1.24+ (build only — the compiled binary has no dependencies)
go build -ldflags="-s -w" -o dahua2mqtt .Cross-compile for a remote Linux server:
GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -ldflags="-s -w" -o dahua2mqtt .Create user and directories:
sudo useradd -r -s /sbin/nologin dahua
sudo mkdir -p /opt/dahua2mqtt /etc/dahua2mqtt /var/log/dahua2mqtt
sudo chown dahua:dahua /var/log/dahua2mqttDeploy binary, config, and service:
sudo cp dahua2mqtt /opt/dahua2mqtt/
sudo chmod 755 /opt/dahua2mqtt/dahua2mqtt
sudo cp config.example.yaml /etc/dahua2mqtt/config.yaml # edit before starting
sudo chown root:dahua /etc/dahua2mqtt/config.yaml
sudo chmod 640 /etc/dahua2mqtt/config.yaml
sudo cp dahua2mqtt.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable --now dahua2mqttVerify:
curl http://localhost:8080/health
# {"mqtt_connected":true,"status":"ok"}
curl http://localhost:8080/stats
# {"version":"2.1.0","events_received":0,"mqtt_connected":true,...}A simple unprivileged LXC (1 vCPU, 64 MB RAM, 4 GB disk) is more than sufficient. The Go binary uses ~5-10 MB of memory at runtime.
The bridge reads /etc/dahua2mqtt/config.yaml by default (override with CONFIG_FILE env var). All keys can also be overridden via uppercase env vars.
See config.example.yaml for a complete example.
| Key | Env var | Default | Description |
|---|---|---|---|
mqtt.host |
MQTT_HOST |
localhost |
Broker hostname/IP |
mqtt.port |
MQTT_PORT |
1883 |
Broker port |
mqtt.username |
MQTT_USERNAME |
Broker username | |
mqtt.password |
MQTT_PASSWORD |
Broker password |
cameras:
- cam1
- cam2List every camera name prefix that appears in your IVS rule names. Four binary sensors are created per camera.
| Key | Env var | Default | Description |
|---|---|---|---|
port |
PORT |
8080 |
HTTP listener port |
logfile |
LOGFILE |
/var/log/dahua2mqtt/dahua2mqtt.log |
Rotating log path |
log_level |
LOG_LEVEL |
INFO |
DEBUG / INFO / WARN / ERROR |
anti_dither |
ANTI_DITHER |
0 |
Suppress repeated events per camera/rule within N seconds (0 = disabled) |
off_delay |
OFF_DELAY |
10 |
Seconds before a binary sensor auto-resets to OFF |
| Key | Env var | Default | Description |
|---|---|---|---|
allowed_ips |
[] |
IP allowlist (empty = allow all) | |
trust_proxy |
TRUST_PROXY |
false |
Use X-Forwarded-For header for IP checks |
trusted_proxies |
[] |
Source IPs allowed to inject X-Forwarded-For (when trust_proxy=true). Empty = trust loopback + RFC1918/ULA private nets only. |
The bridge determines whether a detection is vehicle or human by:
- Checking the event payload fields:
Data.Object.ObjectType,Data.Object.Type,Data.Objects[].ObjectType,Data.Objects[].Type. - Falling back to the IVS rule name convention:
_c_or segmentc→ vehicle_h_or segmenth→ human_ch_or segmentch→ both
- If neither yields a result, both vehicle and human sensors are triggered.
Recognized aliases: human, person → human; vehicle, car, motor vehicle, motorvehicle → vehicle.
On your NVR, under Network → Alarm Center:
- Enable the checkbox.
- Set
Protocol TypetoHTTP. - Set
Server Addressto this bridge's IP (e.g.192.168.1.10). - Set
Portto8080(or whatever you configured). - Hit
Apply.
Then, for each rule under Event → Alarm Center:
- Enable
Report Alarm. - Set
Report AlarmtoHTTP. - Tick
Event.
Dahua NVR does not send channel IDs through the Alarm Center interface. Use the rule name to encode camera identity and object type:
cam1_r1_h_trip→ camera 1, rule 1, human, tripwirecam2_r1_ch_intrusion→ camera 2, rule 1, car + human, intrusioncam1_r1_c_trip→ camera 1, rule 1, vehicle, tripwire
The first segment (before the first _) is used as the camera name and must match an entry in the cameras list.
GET /health
{"status": "ok", "mqtt_connected": true}
GET /stats
{
"version": "2.1.0",
"events_received": 145,
"events_anti_dithered": 3,
"events_ignored": 2,
"mqtt_publish_ok": 140,
"mqtt_publish_fail": 0,
"mqtt_connected": true,
"uptime_seconds": 3600,
"uptime_formatted": "1h 0m",
"config": {
"anti_dither": 5,
"off_delay": 10,
"cameras": ["cam1", "cam2"]
}
}
GET /
dahua2mqtt 2.1.0
Removing a camera from cameras: and restarting the service stops new events
for it but leaves the old retained MQTT discovery and state messages on the
broker — Home Assistant will keep showing those sensors as "ghosts". Use the
included script to clear them:
./reset_sensors.sh purge BROKER_IP <removed-cam-name> [-u user] [-P pass]The same script supports a reset mode that publishes OFF to a sensor's
state topic (without clearing it) — handy when a sensor is stuck ON and
you want HA to see OFF immediately.
- Do NOT expose this bridge to the internet.
- Use
allowed_ipsto restrict to your NVR's IP. - When using
trust_proxy: truebehind a reverse proxy on a non-private IP, populatetrusted_proxieswith the proxy's IP. Otherwise only loopback and RFC1918/ULA addresses are trusted to injectX-Forwarded-For. - Allow outbound traffic only to your MQTT broker.
- The systemd unit enforces: read-only filesystem, no capabilities, restricted syscalls, no home directory access, private /tmp.
MIT