Skip to content

Home Assistant Integration

Home Assistant is Sanctum’s haus automation hub. It runs as a Docker container on the Mac Mini with bridge networking, integrating HomeKit devices and providing remote access through a Cloudflare tunnel. Sonos speakers are managed by a separate native bridge running directly on the Mac, because Docker bridge networking and Sonos callbacks have a relationship best described as “irreconcilable differences.”

The robot butler — your haus automation concierge

┌──────────────────────────────────────────────┐
│ Mac Mini │
│ │
│ ┌─────────────────────────────────────────┐ │
│ │ Sonos Bridge (native, port 18421) │ │
│ │ SoCo → mDNS → 10 Sonos speakers │ │
│ │ XTTS → port 8008 → Yoda voice │ │
│ └──────────┬──────────────────────────────┘ │
│ │ REST (host.docker.internal) │
│ ┌──────────▼──────────────────────────────┐ │
│ │ Docker (bridge network) │ │
│ │ │ │
│ │ ┌─────────────────────────────────┐ │ │
│ │ │ Home Assistant Container │ │ │
│ │ │ Port 8123 → Web UI │ │ │
│ │ │ Port 21063 → HomeKit Bridge │ │ │
│ │ │ SSH agent → VM access │ │ │
│ │ └─────────────────────────────────┘ │ │
│ └─────────────────────────────────────────┘ │
│ │
│ Cloudflare Tunnel → ha.example.net │
└──────────────────────────────────────────────┘

HA was migrated from the VM to Mac Docker because SLIRP networking on the VM proved unreliable for the number of integrations and real-time device communication required. Sonos was later extracted from HA entirely because Docker bridge networking on macOS couldn’t reliably maintain the bidirectional callback connection that Sonos requires. Two migrations, two networking problems, one closet.

The container uses bridge networking rather than --network=host because host networking is not supported on macOS Docker. This is the polite way of saying “Docker on Mac is a compromise and we’ve all agreed to live with it.”

SettingValue
Imageghcr.io/home-assistant/home-assistant:stable
Network modebridge
Web UI port8123
HomeKit port21063
Config directory~/.openclaw/homeassistant/
Restart policyunless-stopped

The compose.yaml at ~/.openclaw/homeassistant/ defines the container with these essential directives:

  • Port mappings: 8123:8123 (web), 21063:21063 (HomeKit)
  • Volume mount: Local config directory into /config
  • Extra hosts: host.docker.internal:host-gateway for reaching Mac services from inside the container
  • SSH agent socket: /run/host-services/ssh-auth.sock mounted for agent forwarding to the VM

Docker Desktop on macOS is a virtualization layer pretending to be native. It runs a Linux VM, routes containers through it, and tells you that --network=host means host networking. It does not. It means the VM’s network — a thing that cannot see mDNS broadcasts, cannot discover HomeKit devices, and cannot be convinced otherwise no matter how many Stack Overflow answers suggest it should.

This matters because HA’s HomeKit Controller discovers Ecobee thermostats and similar devices via Bonjour/mDNS, which broadcasts to the local subnet — a subnet Docker Desktop’s VM is not on.

On March 28, 2026, Docker Desktop was replaced with Colima — a lightweight alternative that is, at its core, just a Lima VM running a Docker daemon. No GUI. No update nags. No Kubernetes toggle that nobody asked for. It starts in 12 seconds via launchd and exposes its Unix socket at:

DOCKER_HOST=unix:///Users/neo/.colima/default/docker.sock

The startup chain is intentionally sequential:

  1. com.sanctum.colima LaunchAgent starts Colima at boot
  2. launch-after-docker.sh waits for the Docker socket to appear
  3. Once the socket is live, it brings up the Home Assistant and signal-cli containers

The migration itself was uneventful, which is the highest compliment infrastructure work can receive. Stop Desktop, start Colima, point DOCKER_HOST at the new socket, bring up the containers. The Ecobee sensors appeared in HA’s discovery panel within seconds. They had been broadcasting the entire time. Docker Desktop just couldn’t hear them.

Sanctum manages 10 Sonos speakers across the haus. The speakers are not controlled through HA’s built-in Sonos integration. That integration is disabled. Instead, a native Python service called the Sonos Bridge runs directly on the Mac Mini, bypassing Docker entirely.

The bridge runs on port 18421 and exposes a REST API for speaker control, TTS announcements, and media playback. HA communicates with it via rest_command entries in configuration.yaml.

┌─────────────────────────────────────────────┐
│ Mac Mini (native) │
│ │
│ ┌───────────────────────────────────────┐ │
│ │ Sonos Bridge (port 18421) │ │
│ │ SoCo → mDNS → 10 speakers on LAN │ │
│ │ XTTS → Yoda TTS voice generation │ │
│ └──────────┬────────────────────────────┘ │
│ │ REST API │
│ ┌──────────▼────────────────────────────┐ │
│ │ Docker: Home Assistant │ │
│ │ rest_command.sonos_tts → :18421/tts │ │
│ │ rest_command.sonos_announce → :18421 │ │
│ └───────────────────────────────────────┘ │
└─────────────────────────────────────────────┘

Docker bridge networking on macOS cannot relay mDNS or SSDP discovery packets. The HA integration requires advertise_addr and static host IPs to compensate, but Sonos speakers still need to reach back into the container for event callbacks. This callback path breaks unpredictably after container restarts, Colima VZ networking warmups, and the occasional firmware update. We tried socat tunnels, socket_vmnet, and timed reload automations. None of it was reliable.

The native bridge uses SoCo directly on the Mac’s LAN interface. mDNS discovery works instantly. Speaker callbacks land on the Mac’s real IP. No tunnels, no NAT, no prayers.

HA scripts and automations call the bridge through two REST commands:

rest_command:
sonos_tts:
url: "http://host.docker.internal:18421/tts"
method: POST
content_type: "application/json"
payload: '{"rooms": {{ rooms }}, "text": "{{ message }}", "severity": "{{ severity }}"}'
sonos_announce:
url: "http://host.docker.internal:18421/announce"
method: POST
content_type: "application/json"
payload: '{"rooms": {{ rooms }}, "text": "{{ message }}", "severity": "{{ severity }}"}'

Severity maps to volume: low (30%), medium (50%), high (80%), critical (100%). The bridge handles XTTS generation, audio file serving, and Sonos playback in a single call.

The bridge generates speech using the XTTS-v2 server on port 8008, saves the WAV file locally, and tells Sonos to fetch it from http://<mac-lan-ip>:18421/<id>.wav. The voice is Yoda. There was a discussion about this. Yoda won.

Terminal window
# Check bridge health
curl -s http://localhost:18421/health | python3 -m json.tool
# List all speakers
curl -s http://localhost:18421/speakers | python3 -m json.tool
# Manual TTS test
curl -X POST http://localhost:18421/tts \
-H 'Content-Type: application/json' \
-d '{"rooms": ["living_room"], "text": "Testing", "severity": "low"}'
# Restart bridge
launchctl kickstart -k gui/$(id -u)/com.openclaw.sonos-bridge

The HA container needs SSH access to the VM for certain automations and scripts. This is accomplished by mounting the Mac’s SSH agent socket into the container.

The SSH agent socket at /run/host-services/ssh-auth.sock is mapped as a volume in the compose file. Scripts inside the container can then SSH to ubuntu@10.10.10.10 using the Mac’s loaded SSH keys without storing any private keys in the container.

SSH wrapper scripts live at ~/.openclaw/ha-scripts/ and exec SSH directly to the VM with agent forwarding enabled.

The configuration.yaml includes network-specific settings for the bridge networking environment:

homeassistant:
internal_url: "http://192.168.1.10:8123"
external_url: "https://ha.example.net"
http:
use_x_forwarded_for: true
trusted_proxies:
- 172.16.0.0/12
- 192.168.64.0/20

The trusted proxies cover Docker’s internal bridge subnets, allowing HA to correctly identify client IPs when accessed through the Cloudflare tunnel or Docker’s bridge network.

There are two remote access paths into HA, each serving a different purpose. Because one way in would have been too simple, and simplicity is what other people have.

For accessing the HA web UI from a browser, a Cloudflare tunnel provides HTTPS with Zero Trust access controls.

ComponentDetails
Tunnel namesanctum-hub
LaunchAgentcom.sanctum.tunnel
External URLhttps://ha.example.net
Routeha.example.net to localhost:8123
AuthCloudflare Access OTP

The HA Companion app cannot handle Cloudflare Access’s login page, so it connects via Tailscale instead. Tailscale provides end-to-end WireGuard encryption, so HTTPS is not needed.

  1. Install Tailscale on iPhone and connect (do NOT enable exit node)

  2. Install HA Companion

  3. Open HA Companion → Settings → Companion App → Server & Connection

  4. Configure connection URLs:

    FieldValue
    Internal URLhttp://<mac-lan-ip>:8123
    External URLhttp://<tailscale-ip>:8123
  5. Under Internal Connection: add your haus Wi-Fi SSID so the app uses the LAN when haus

  6. Enable Local Push for instant notifications on haus Wi-Fi

  7. Test: turn off Wi-Fi briefly — the app should reconnect via Tailscale

The HomeKit bridge runs on port 21063, allowing Apple Home app control of HA-managed devices.

SettingValue
Port21063
Pairing code840-97-709

HomeKit entities are configured within HA’s integration settings. The bridge exposes selected devices to Apple Home while keeping the full HA device list available through the web UI.

Terminal window
# Start HA
cd ~/.openclaw/homeassistant && docker compose up -d
# Stop HA
cd ~/.openclaw/homeassistant && docker compose down
# View logs
docker logs -f homeassistant
# Restart
cd ~/.openclaw/homeassistant && docker compose restart

If the Mac Docker instance has issues, a rollback path exists to the VM:

  1. Stop the Mac container: docker compose down
  2. SSH to the VM: ssh openclaw
  3. Start the VM instance: ~/.openclaw/ha-docker.sh recreate