Building a Local-First Smart Home with Home Assistant

From a bare KVM VM to facial recognition door greetings — how I built a fully self-hosted Home Assistant setup on ELITEBOOK with Frigate, CompreFace, and zero cloud dependency.

home-assistanthomelabfrigatedockernetworking

I’ve always been suspicious of smart home platforms that phone home. The idea of a subscription standing between me and my own light switches doesn’t sit right. So when I decided to actually build out a smart home, the answer was obvious: Home Assistant, self-hosted, fully local, no cloud required.

What followed was several months of rabbit holes — networking quirks, camera pipelines, facial recognition, and a VM that kept finding new ways to surprise me. This is that story.

The Foundation: HAOS in a KVM VM

Home Assistant runs best on its own dedicated OS (HAOS), which means it wants to own the machine. Since ELITEBOOK is already running a full self-hosted stack, the solution was a KVM/libvirt VM — HAOS gets its own environment, isolated from everything else, but sharing the hardware.

Setting up the VM itself is straightforward enough. The interesting part is networking.

The macvtap Rabbit Hole

Most people run their HA VM behind NAT — simple, works fine. I wanted HAOS to appear as a first-class device on my LAN with its own IP, so other devices could discover it via mDNS without any bridging tricks. That meant macvtap.

macvtap creates a virtual NIC that bridges directly onto a physical interface at layer 2. The VM gets a real MAC address (52:54:00:3b:44:e6), reserved in eero at 192.168.4.222, and appears on the network like any other device. Clean.

Except for one gotcha that bit me immediately: the host cannot reach a macvtap-attached VM directly. This is a kernel-level isolation property — traffic from the host to the VM’s MAC never loops back through the physical switch. So from ELITEBOOK, the HA VM isn’t reachable at 192.168.4.222. The workaround is virbr0, libvirt’s NAT bridge — the VM is also reachable at 192.168.122.18 from the host side.

This matters later when wiring up services. Anything inside the HA VM that needs to reach ELITEBOOK-hosted services (Frigate on :8971, CompreFace on :8000) has to use 192.168.122.1. Anything on the LAN talks to 192.168.4.222. Two different addresses, same VM — keep that straight and everything works.

The USB NIC I was originally using for macvtap also caused problems — the r8152 driver would silently drop links under sustained load. Eventually migrated the whole stack to the built-in NIC (enp1s0f1) and rebuilt macvtap on top of that. Zero issues since.

Frigate: Local NVR with Real Object Detection

Once HA was stable, cameras were next. Frigate is a local NVR built specifically for Home Assistant integration — it runs object detection on camera streams and fires events that HA can act on.

Frigate runs as a Docker container on ELITEBOOK alongside the rest of the stack, not inside the VM. It connects to cameras via RTSP, runs detection using the CPU (no Coral or GPU yet — that’s a future upgrade), and exposes its API and RTMP streams on port 8971.

The HA integration connects to Frigate over the internal network. Since the VM can’t reach ELITEBOOK at 192.168.4.66 directly (macvtap isolation), it uses 192.168.122.1 — the virbr0 gateway. Once that’s wired up, Frigate’s cameras, events, and clips all appear natively in the HA UI.

One early headache: the Mosquitto MQTT broker that Frigate uses for events. BusyBox’s nc (used in healthchecks inside Alpine containers) doesn’t support the -z flag that most netcat healthcheck examples use. The correct healthcheck ended up being:

healthcheck:
  test: ["CMD-SHELL", "nc -w1 127.0.0.1 1883 < /dev/null"]

Small thing, hours lost.

CompreFace + Double Take: Teaching HA Who’s at the Door

Object detection tells you something is at the door. Facial recognition tells you who. That’s where CompreFace and Double Take come in.

CompreFace is an open-source facial recognition service — runs locally, no cloud, just a Docker container with a REST API. You train it with photos of the people in your household, and it returns names with confidence scores.

Double Take sits between Frigate and CompreFace. When Frigate detects a person, it sends the snapshot to Double Take, which runs it through CompreFace and publishes the result back to MQTT. Home Assistant picks up the MQTT event and knows who was just detected.

Training is simple: upload a handful of photos per person through the CompreFace UI, give each subject a name. I trained it on myself (Matt), Meredith, and Colten. A few dozen photos each, taken in different lighting conditions, gets recognition accuracy to a reliable level.

The Greeting Automations

With facial recognition working, the obvious next step was door greetings. When the front door camera sees a known face, HA announces who just arrived over the house speakers.

The automation logic sounds simple but has some nuance. The naive version — trigger on any CompreFace detection — fires constantly, including when someone’s just standing near a window or the detection flickers. The better version:

  1. Trigger on Double Take sensor state change (not Frigate motion — too noisy)
  2. wait_for_trigger on the front door contact sensor opening, with a timeout
  3. continue_on_timeout: false — if the door doesn’t open, don’t announce
  4. Fire the TTS announcement only when both conditions are met

That way you only get a greeting when someone is actually detected and the door opens — not every time the camera catches a face through the window.

Four automations total: Greet Matt, Greet Meredith, Greet Colten, and a variant for Colten arriving from his dad’s place (triggered differently based on which zone he’s coming from).

Cloudflare Tunnel: Secure Remote Access

The last piece was remote access. Rather than punching holes in the firewall or messing with dynamic DNS, I use a Cloudflare tunnel running as a HA addon. The addon (cloudflared) runs inside the HAOS VM and maintains an outbound-only connection to Cloudflare’s edge. Remote access to HA goes through that tunnel — no open ports, no VPN required.

The addon management uses ha apps commands (not ha addons — caught that one the hard way). The tunnel itself is managed via the remote API from the same Cloudflare account that handles the rest of the infrastructure, so all the public hostnames live in one place.

Where Things Stand

The stack today:

  • HAOS running in a KVM VM, stable on 192.168.4.222
  • Frigate doing 24/7 object detection on all cameras
  • CompreFace + Double Take identifying faces at the front door
  • Greeting automations welcoming everyone home by name
  • Cloudflare tunnel for secure remote access

It’s entirely local. No cloud subscriptions. No data leaving the house. And it’s still evolving — the next planned upgrade is a dedicated GPU box for hardware-accelerated Frigate inference, which will let detection run faster and cover more cameras without taxing the CPU.

That’s a post for another day.