Skip to main content
Back to blog

Running Home Assistant for smart home automation

·7 min readHomelab

I resisted smart home stuff for a long time. Most of it requires a cloud account, phones home constantly, and breaks when the vendor shuts down. The Philips Hue bridge talks to Philips servers. The Tuya app talks to Tuya servers. Your light switch should not depend on an AWS region staying up.

Home Assistant changed my mind. It runs locally, supports thousands of devices, and does not phone home. I run it in Docker alongside the rest of my self-hosted stack, and it has become the central nervous system for everything in my house.

Why Home Assistant

Home Assistant is an open-source home automation platform written in Python. It runs a web interface on your local network where you can control devices, view sensor data, and build automations. The project has a huge community and integrates with over 2,000 device types out of the box.

The key selling point for me: everything stays local. Zigbee devices talk to a USB coordinator plugged into my server. MQTT messages never leave my network. Automations run on my hardware, not someone else's cloud. If my internet goes down, the lights still work.

The Zigbee stack

Before setting up Home Assistant, you need a way to talk to Zigbee devices. The setup has three parts:

  • A Zigbee USB coordinator: I use the Sonoff Zigbee 3.0 USB Dongle Plus (ZBDongle-P), based on the TI CC2652P chip. It costs about $20 and works out of the box with Zigbee2MQTT.
  • Zigbee2MQTT: A bridge that connects to the USB coordinator and translates Zigbee device messages into MQTT.
  • Mosquitto: A lightweight MQTT broker that routes messages between Zigbee2MQTT and Home Assistant.

This might seem like a lot of moving parts for turning on a light. But the separation is what makes it flexible. Any device that speaks MQTT can participate in the system, including custom hardware like the LoRa weather station I built.

Docker Compose setup

Here is the full docker-compose.yml for all three services:

services:
  homeassistant:
    image: ghcr.io/home-assistant/home-assistant:stable
    container_name: homeassistant
    volumes:
      - ./ha-config:/config
      - /etc/localtime:/etc/localtime:ro
    network_mode: host
    restart: unless-stopped
 
  mosquitto:
    image: eclipse-mosquitto:2
    container_name: mosquitto
    ports:
      - "1883:1883"
    volumes:
      - ./mosquitto/config:/mosquitto/config
      - ./mosquitto/data:/mosquitto/data
      - ./mosquitto/log:/mosquitto/log
    restart: unless-stopped
 
  zigbee2mqtt:
    image: koenkk/zigbee2mqtt
    container_name: zigbee2mqtt
    volumes:
      - ./zigbee2mqtt-data:/app/data
      - /run/udev:/run/udev:ro
    ports:
      - "8080:8080"
    devices:
      - /dev/serial/by-id/usb-Silicon_Labs_Sonoff_Zigbee_3.0_USB_Dongle_Plus-if00-port0:/dev/ttyACM0
    environment:
      - TZ=Europe/Berlin
    depends_on:
      - mosquitto
    restart: unless-stopped

A few things to note:

Home Assistant uses network_mode: host because it needs to discover devices on the local network via mDNS and SSDP. Running it on the bridge network breaks device discovery. The web UI is available at http://your-server-ip:8123.

The Zigbee USB dongle is passed through using the devices key. Always use the /dev/serial/by-id/ path, not /dev/ttyACM0. The by-id path is stable across reboots. The ttyACM number can change if you plug in another USB device.

Mosquitto needs a config file or it will reject connections. Mosquitto 2.0+ defaults to local-only mode. Create mosquitto/config/mosquitto.conf:

listener 1883
allow_anonymous true
persistence true
persistence_location /mosquitto/data/
log_dest file /mosquitto/log/mosquitto.log

The allow_anonymous true is fine for a home network where the broker is not exposed to the internet. For anything else, set up password authentication.

Initial setup

Run docker compose up -d and open http://your-server-ip:8123 in a browser. Home Assistant walks you through creating an account, setting your location (for sunrise/sunset automations), and choosing which integrations to enable.

After the onboarding, add the MQTT integration: go to Settings, Devices & Services, Add Integration, search for MQTT. Point it at your Mosquitto broker (the IP of your Docker host, port 1883). Home Assistant will automatically discover devices that Zigbee2MQTT publishes.

Configuring Zigbee2MQTT

Create zigbee2mqtt-data/configuration.yaml:

mqtt:
  base_topic: zigbee2mqtt
  server: mqtt://172.17.0.1:1883
homeassistant: true
permit_join: false
serial:
  port: /dev/ttyACM0
  adapter: zstack
frontend:
  port: 8080

The server address 172.17.0.1 is the Docker bridge gateway, which reaches the Mosquitto container's published port. Setting homeassistant: true enables MQTT auto-discovery, so devices paired in Zigbee2MQTT automatically appear as entities in Home Assistant.

Set permit_join: true temporarily when pairing new devices. The Zigbee2MQTT web UI at port 8080 shows connected devices, signal strength, and lets you rename them.

Pairing a device

With permit_join enabled, put your Zigbee device in pairing mode (usually by holding a button for 5 seconds). Zigbee2MQTT picks it up within a few seconds and publishes it to MQTT. Home Assistant discovers it automatically.

I started with a couple of IKEA TRADFRI smart plugs and an Aqara temperature sensor. Both paired on the first try. The Aqara sensor reports temperature, humidity, pressure, and battery level. All of these show up as separate entities in Home Assistant.

Practical automations

Home Assistant automations have three parts: triggers, conditions, and actions. You can build them in the UI or write YAML directly in automations.yaml. I prefer YAML because I can version control it.

Turn on living room lights at sunset:

- alias: "Living room lights at sunset"
  trigger:
    - platform: sun
      event: sunset
      offset: "-00:30:00"
  condition:
    - condition: state
      entity_id: person.jeremy
      state: "home"
  action:
    - service: light.turn_on
      target:
        entity_id: light.living_room
      data:
        brightness_pct: 70

This triggers 30 minutes before sunset, but only if I am home. The person.jeremy entity comes from the Home Assistant companion app on my phone, which reports location.

Temperature alert from the garden sensors:

In my LoRa weather station post, I mentioned wanting to pull sensor data into Home Assistant. The receiver already pushes data to InfluxDB over HTTP. To get it into Home Assistant, I publish the same data to MQTT. Home Assistant picks it up as a sensor entity through MQTT auto-discovery.

- alias: "Garden freeze warning"
  trigger:
    - platform: numeric_state
      entity_id: sensor.garden_temperature
      below: 2
  action:
    - service: notify.mobile_app_jeremy_phone
      data:
        title: "Freeze warning"
        message: "Garden temperature is {{ states('sensor.garden_temperature') }}°C"

Motion-activated hallway light:

- alias: "Hallway motion light"
  trigger:
    - platform: state
      entity_id: binary_sensor.hallway_motion
      to: "on"
  condition:
    - condition: sun
      after: sunset
      before: sunrise
  action:
    - service: light.turn_on
      target:
        entity_id: light.hallway
      data:
        brightness_pct: 40
    - delay: "00:03:00"
    - service: light.turn_off
      target:
        entity_id: light.hallway

This only activates at night. During the day, the hallway gets enough natural light. The 3-minute delay before turning off is short enough to not waste power but long enough that you are usually past the hallway by then.

The maintenance reality

Home Assistant is powerful, but it is not set-and-forget.

Updates. The project releases monthly. Most updates are smooth. Occasionally one breaks an integration or changes how YAML automations work. I let updates sit for a week and check the release notes before applying them. The Docker setup makes rollbacks easy: just change the image tag.

YAML vs UI. You can build automations in the UI or in YAML files. The UI is fine for simple stuff, but complex automations with templates and conditions are easier to reason about in YAML. The downside: the UI and YAML automations live in different places, which gets confusing if you mix them.

Debugging. When an automation does not fire, check the automation trace in the UI. It shows exactly which trigger matched, which conditions passed or failed, and what actions ran. This saves a lot of guessing.

Zigbee mesh health. Zigbee devices form a mesh network. Battery-powered sensors connect through mains-powered devices (like smart plugs) to extend range. If you remove a smart plug, sensors behind it might lose connectivity. The Zigbee2MQTT network map visualizes the mesh topology, which helps when troubleshooting dropped devices.

Home Assistant is one of those projects where you spend a weekend setting it up, then spend the next six months adding "just one more automation." The Pi-hole I set up for network-wide ad blocking was the same way. These homelab services have a way of multiplying.

Sources

Enjoying the blog? Subscribe via RSS to get new posts in your reader.

Subscribe via RSS