Migration Checklist
Use this checklist when migrating each app from AppDaemon to Hassette. Work through one app at a time — verify it works before moving to the next.
The Migration Guide overview covers pre-migration setup (installing Hassette, reviewing the mental model). This checklist picks up from there.
Before You Start
- [ ] Requires Python 3.11 or later — check with
python --versionorpython3 --version. Hassette will not install on Python 3.10 or earlier. See python.org/downloads to upgrade.
Step 1: Configuration
- [ ] Convert
appdaemon.yamlconnection settings tohassette.toml[hassette]section - [ ] Convert each app entry in
apps.yamlto an[apps.your_app]table inhassette.toml - [ ] Create a typed
AppConfigsubclass for each app — move allself.args["args"]["key"]accesses toself.app_config.key - [ ] Verify required fields raise a clear error if missing (run the app without a required config key)
See Configuration for the full conversion guide.
Step 2: App Structure
- [ ] Change base class from
Hass(orADAPI) toApp(async) orAppSync(sync) - [ ] Rename
initialize()to the correct hook for your base class:App:async def on_initialize(self)— must beasync defAppSync:def on_initialize_sync(self)— must be a plain synchronous method; do not overrideon_initializeonAppSync(it is@finaland raisesNotImplementedError)
- [ ] If you have
terminate(), rename it:App:async def on_shutdown(self)AppSync:def on_shutdown_sync(self)
- [ ] Confirm the app starts without errors (
uv run hassette runor your start command)
See Mental Model for the lifecycle differences.
Step 3: Event Listeners
- [ ] Convert each
self.listen_state(...)toawait self.bus.on_state_change(...) - [ ] Add
await— allself.bus.on_*()methods are async and must be awaited - [ ] Add
name=— a stable string identifier is required on every registration (e.g.name="kitchen_light") - [ ] Move filter arguments:
new=→changed_to=,old=→changed_from= - [ ] Update callback signatures to use dependency injection or accept an event object
- [ ] Replace
self.cancel_listen_state(handle)withsubscription.cancel() - [ ] Convert each
self.listen_event("call_service", ...)toawait self.bus.on_call_service(...) - [ ] Add
awaitandname= - [ ] Update callback signatures
- [ ] Replace
self.cancel_listen_event(handle)withsubscription.cancel() - [ ] For attribute-level subscriptions, switch to
await self.bus.on_attribute_change(...)
See Bus & Events for side-by-side examples.
Step 4: Scheduler
- [ ] Convert each
self.run_in(cb, seconds)toself.scheduler.run_in(cb, delay=seconds) - [ ] Convert each
self.run_once(cb, time(H, M))toself.scheduler.run_once(cb, at="HH:MM") - [ ] Convert each
self.run_every(cb, "now", interval)toself.scheduler.run_every(cb, seconds=interval) - [ ] Convert each
self.run_daily(cb, time(H, M))toself.scheduler.run_daily(cb, at="HH:MM") - [ ] Replace
self.cancel_timer(handle)withjob.cancel()on the returnedScheduledJob - [ ] Check any blocking work inside callbacks — for apps with heavy sync logic, switch to
AppSync; for isolated blocking calls inside anApphandler, useawait self.task_bucket.run_in_thread(...)
See Scheduler for method equivalents.
Step 5: API Calls
- [ ] Convert
self.get_state(entity_id)toself.states.domain.get(entity_id)for cached reads - [ ] Replace
self.call_service("domain/service", ...)withawait self.api.call_service("domain", "service", ...) - [ ] Add
awaitto allself.api.*calls — forgettingawaitreturns a coroutine without executing the call - [ ] Replace
self.set_state(...)withawait self.api.set_state(...) - [ ] Replace
self.log(...)withself.logger.info(...)(and.warning(),.error()as needed)
See API Calls for the full guide.
Step 6: Test
- [ ] Write at least one test using
AppTestHarness - [ ] Seed entity state before simulating events
- [ ] Simulate the key events your app responds to
- [ ] Assert the expected API calls were made via
harness.api_recorder - [ ] Run the test suite:
pytest
See Testing for the test harness guide.
Step 7: Verify Live
- [ ] Deploy the migrated app alongside (or instead of) the AppDaemon version
- [ ] Confirm all automations fire as expected in live operation
- [ ] Check logs for any runtime errors or unexpected behavior
Common Pitfalls
Async gotchas
- Forgetting
awaitonself.api.*calls is the most common migration mistake. The call returns a coroutine object and silently does nothing. - In
AppSync, use.syncfacades for bus, scheduler, and API —self.bus.sync.on_state_change(...),self.scheduler.sync.run_in(...),self.api.sync.call_service(...). Calling the async methods directly from sync hooks returns un-awaited coroutines that silently do nothing. - Do not use
.syncfacades insideApplifecycle methods — use the async API instead, or switch toAppSync.
Configuration access
- AppDaemon:
self.args["args"]["key"] - Hassette:
self.app_config.key - Define all config keys in your
AppConfigmodel for validation and autocomplete.
State access
- AppDaemon:
self.get_state()returns a cached state (string or dict) - Hassette:
self.states.light.get("light.kitchen")returns a typed cached state (noawaitneeded) (the domain prefix is optional). - Use
self.api.get_state()only when you need to force a fresh read from Home Assistant.
Next Steps
After migrating all your apps:
- Review the Core Concepts to learn the full Hassette feature set
- Explore Dependency Injection, Custom States, and Type Registries
- Set up the Web UI for live monitoring of your automations