Watching the doorbell with Go

My doorbell is a Dahua-based Amcrest unit, and like most cameras in that family it speaks a chatty, slightly archaic HTTP API. The vendor app works, but I wanted button presses flowing into Home Assistant with as little latency and as few moving parts as possible. That itch became dahua-companion, a small Go service with a deliberately tiny product surface: when someone presses the doorbell, publish to doorbell/pressed. One event, one MQTT topic, empty payload — the event itself is the signal.

The event stream hiding in the camera

Dahua firmware exposes a long-poll subscription endpoint:

/cgi-bin/eventManager.cgi?action=attach&codes=[AlarmLocal]&heartbeat=5

You attach with HTTP digest auth and the camera pushes event lines as they happen — no polling, no cloud round-trip. A press arrives on your LAN as Code=AlarmLocal;action=Start;... milliseconds after the button is pushed. Subscribing to codes=[AlarmLocal] narrows the firehose down to the one event I care about, and the parser only fires on the Start action so Stop and Pulse lines don't double-ring the automation.

Most of the code is about failure

The happy path is maybe twenty lines. The rest of the service exists because both ends of the pipe fail in quiet, annoying ways:

  • Cameras reboot, and TCP doesn't notice. After a camera restart the socket can look open for minutes before OS keepalives give up. So the stream is wrapped in a watchdog reader that resets a timer on every read — the 5-second heartbeat keeps it fed, and 15 silent seconds tears the connection down for a reconnect with capped backoff.
  • MQTT clients lie about being connected. With Paho's default 30-second keepalive, a dead broker socket happily swallows publishes before anyone notices. The keepalive is tuned down to 10 seconds with a 5-second ping timeout, and reconnect intervals are capped at 30 seconds instead of the default 10 minutes.
  • A late doorbell event is worse than no event. If the broker is down, queued presses expire after 30 seconds rather than deliver late — a doorbell press only matters while someone might still be at the door. Staleness beats durability here, which is the opposite of most queueing advice.

The pleasant parts

Home Assistant support is just MQTT discovery: the service publishes a retained config describing a doorbell event entity, plus an availability topic backed by a last-will message, so the entity shows up (and goes offline honestly) with zero YAML on the Home Assistant side.

Deployment is a static binary in a FROM scratch image — no shell, no runtime. Since scratch has no curl, the container healthcheck re-invokes the binary itself with a -healthcheck flag against its own /health endpoint, which only reports healthy when the MQTT connection, the event stream, and a live probe of the camera all check out.

Keeping it small on purpose

The most useful design decision was refusing to let the tool grow. It does not transcode video, store clips, or try to be a dashboard — other tools do those jobs well. There's very little code, so there is very little to break, and nearly every constant in the repo carries a comment explaining why it has that value. If you have a Dahua or Amcrest doorbell, the repo is on GitHub — issues and PRs welcome.