Skip to content

Migration Guide

This guide covers migrating AppDaemon automations to Hassette.

Quick Reference

Four areas change: configuration, app structure, event handlers, and API calls. Hassette splits AppDaemon's flat self.* surface into typed handles: self.bus (event subscriptions), self.scheduler (timed jobs), self.api (HA service calls), and self.states (entity state cache).

Action AppDaemon Hassette Guide
Define an app class MyApp(Hass) class MyApp(App[MyConfig]) Configuration
Lifecycle hook def initialize(self): async def on_initialize(self): Mental Model
Listen for state changes self.listen_state(self.cb, "light.x", new="on") await self.bus.on_state_change("light.x", handler=self.cb, changed_to="on", name="...") Bus & Events
Listen for service calls self.listen_event(self.cb, "call_service", domain="light") await self.bus.on_call_service(domain="light", handler=self.cb, name="...") Bus & Events
Cancel a listener self.cancel_listen_state(handle) subscription.cancel() Bus & Events
Schedule a timer self.run_in(self.cb, 60) await self.scheduler.run_in(self.cb, delay=60) Scheduler
Cancel a timer self.cancel_timer(handle) job.cancel() Scheduler
Run daily at 07:30 self.run_daily(self.cb, time(7, 30, 0)) await self.scheduler.run_daily(self.cb, at="07:30") Scheduler
Call a HA service self.call_service("light/turn_on", entity_id="light.x") await self.api.call_service("light", "turn_on", target={"entity_id": "light.x"}) API Calls
Get entity state self.get_state("light.x") self.states.light.get("light.x") or await self.api.get_state("light.x") API Calls
Access app config self.args["entity"] self.app_config.entity Configuration
Logging self.log("message") self.logger.info("message") Mental Model

name= in the bus rows above is required — it identifies the listener in logs and the monitoring UI. Use a descriptive string like "kitchen_motion". Omitting it raises ListenerNameRequiredError at runtime. AppDaemon has no equivalent.

All Hassette bus, scheduler, and API calls are async and need await. In AppDaemon, self.listen_state registers immediately. In Hassette, forgetting await means nothing registers — no error, no warning, just silence.

Is Migration Worth It?

Migrate if... Stay with AppDaemon if...
You want IDE autocomplete and type errors at write time Your apps work and you don't need type safety
You want to unit-test automations with a real test harness You prefer synchronous code without async/await
You want Pydantic-validated config with clear error messages Your team already knows AppDaemon well
You want dependency injection in event handlers You rely on AppDaemon features not yet in Hassette
You want structured per-app logs with method and line context

Known Gaps

AppDaemon feature Status in Hassette
listen_log / log event subscriptions Not planned
HADashboard Not planned
Notification helpers (notify, call_action) Use await self.api.call_service("notify", ...) directly
MQTT plugin Not yet supported. No workaround available.
Global variables / inter-app communication Use await self.bus.emit(topic, data) for in-process broadcast

If a feature you depend on is missing, open an issue or check GitHub discussions.

Common Pitfalls

name= is required on all bus subscriptions. Omitting it raises ListenerNameRequiredError at runtime. Every on_state_change, on_call_service, and on call needs a stable string name.

self.api.*, self.bus.on_*, and self.scheduler.* are async and must be awaited. Forgetting await returns a coroutine object. Nothing is registered or called.

AppSync apps use .sync facades. If you subclass AppSync for synchronous handlers, use self.bus.sync.on_state_change(...) and self.scheduler.sync.run_in(...). The async methods are not available in sync hooks.

Per-App Migration Checklist

The Migration Checklist walks through converting a single app from AppDaemon to Hassette. Work through it once for your first app, then use it as a reference for the rest.