Why I moved from Hubitat to Home Assistant
If you’re choosing between Hubitat and Home Assistant, weigh how easily an LLM can work with each one above anything else right now. Home Assistant wins on that. An automation is plain YAML in a file, so I keep the config in git, an agent edits it, and I review the diff. On Hubitat the logic lives behind a UI you can’t diff or point a tool at, and its newer AI rule builder turns a typed request into a rule you then adjust in that same UI.
The models are good at Home Assistant automations too, because they’ve learned from years of community examples. Debugging one used to take an afternoon, and now it’s a quick back-and-forth. I’ve been fixing automations I’d let sit broken for years and writing more ambitious new ones, like my leak alerts.
Running both
I ran a Hubitat hub for years and liked it. Somewhere along the way I stood up Home Assistant too, because it handled a few things better (some Aqara sensors, mostly), and for a long time I was happy running both.
The Hubitat integration for Home Assistant made that easy. It’s a HACS integration that pulls Hubitat devices into Home Assistant over Hubitat’s Maker API. Any device I exposed there showed up in Home Assistant with its state, so Home Assistant could see everything while Hubitat still ran the radios.
If you’re already on Hubitat, running both is a fine way to migrate a device at a time. Two hubs give you more to keep in sync, though. Renaming an entity across both is clunky, and removing a device from the Maker API leaves its Home Assistant entities behind as unavailable until you remove the integration. That, and how well Grafana monitors Home Assistant itself, pushed me to go all-in.
Config in git
I run Home Assistant Container with Docker Compose on a box I SSH into. The compose file and Home Assistant’s config directory sit in the same repo, so deploying a change starts with a git pull on the box:
homelab/
.gitignore # ignores every service's data/ except this one
home-assistant/
docker-compose.yml # mounts ./data as /config
data/ # Home Assistant's config directory
.gitignore # the allowlist below
configuration.yaml
automations.yaml # the automation editor saves here
scripts.yaml
scenes.yaml
dashboards/
secrets.yaml # ignored
.storage/ # ignored
home-assistant_v2.db # ignored
The YAML is what you edit, and it’s the context an agent needs. The rest is state Home Assistant writes itself, like the device registry and auth tokens in .storage/ and the history database. Home Assistant’s own backups cover that, so git only gets the YAML.
My repo uses two .gitignore files for that. The one at the repo root ignores every service’s data/ directory, since those are Docker volumes full of state, and re-includes Home Assistant’s. Git can’t re-include a file if its parent directory is excluded, so without !home-assistant/data/ nothing under home-assistant/data/ would be tracked:
**/data/
!home-assistant/data/
The allowlist is home-assistant/data/.gitignore, and its patterns only apply inside that directory. It ignores everything and allows the YAML back in, so a new file Home Assistant drops there stays out of git by default. If the config directory is your whole repo, this is the only file you need:
*
# * matches directories too, and git won't look inside an ignored one
!*/
!*.yaml
!.gitignore
# passwords and API keys, referenced from other files with !secret
secrets.yaml
# code installed through HACS
custom_components/
# ESPHome device configs, which can hold Wi-Fi passwords and device keys
esphome/
Dashboards and helpers you create in the UI land in .storage/, so I define mine in configuration.yaml instead. They end up in git and come back with a fresh clone, though the UI can’t edit them after that.
After a pull, most changes only need a reload. For an automations.yaml change I use Quick reload (Settings, the three-dot menu, Restart Home Assistant, Quick reload), which checks the config first and applies the YAML that supports reloading without a restart. A change it can’t apply, like adding an integration to configuration.yaml, needs a restart, and Home Assistant’s docs suggest the full check from the container before that:
docker exec homeassistant python -m homeassistant --script check_config --config /config
A Green or Yellow runs Home Assistant OS, the other supported install type. It doesn’t give you a regular shell, so pulling from git takes extra setup. I haven’t tried it, but the official Git pull app pulls the config from a repo (it can’t push), and the Advanced SSH & Web Terminal app ships git if you’d rather commit from the box.
Guardrails for agent edits
Home Assistant’s GUI editor rewrites automations.yaml on every save, so the top of the file tells the agent how to edit it without churning the next save:
# The automation editor rewrites this file on every save and strips comments.
# Write it back in the editor's format so a save only changes what it touched:
#
# yaml.dump(data, Dumper=yaml.CSafeDumper, sort_keys=False,
# default_flow_style=False, allow_unicode=True)
#
# Put an automation's reasoning in its description:, which survives a save.
A GUI save strips this header too, so I restore it from git afterward.
The repo’s AGENTS.md points the agent at that header and tells it to run make check before committing, the same setup I describe in Grounding a coding agent in reality. Here make check runs prek’s check-yaml hook, which catches a YAML syntax error before Home Assistant does. It needs --unsafe to accept Home Assistant’s !include and !secret tags:
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v6.0.0
hooks:
- id: check-yaml
args: [--allow-multiple-documents, --unsafe]
- id: detect-private-key # the config is in git, so block pasted keys
An agent can also skip the files. The unofficial ha-mcp server creates and edits automations over Home Assistant’s API, while the official MCP Server integration only controls the entities you expose to Assist. I stick with files in git for the diffs.
Migrating off Hubitat
I moved Z-Wave to the Home Assistant Connect ZWA-2, Home Assistant’s official Z-Wave adapter. It supports Z-Wave Long Range and is a real range upgrade over my Hubitat C-7’s internal antennas. The Model C-8 added external antennas and an 800-series Z-Wave radio, so a current Hubitat may close some of that gap.
The C-7’s Z-Wave network couldn’t come with me. Hubitat’s Z-Wave backup needs a subscription and only restores to another Hubitat, and Home Assistant can’t use the hub’s internal radio, so every Z-Wave device needed a fresh inclusion on the ZWA-2. A few things made that easier:
- A general exclusion from the ZWA-2 freed a device that was still joined to Hubitat, so I didn’t need the old hub near it.
- Each inclusion creates new entity IDs, so check automations that hard-code them. An action aimed at a missing entity only logs a warning, so a broken reference is easy to miss.
- Removing a Zigbee device cleanly from Hubitat sends a Leave, and an open ZHA join window often catches it right away. A device removed while unreachable keeps its old network key and needs its own reset before it can join.