Skip to content

Apps — Lifecycle

Hassette manages app initialization and shutdown. The app declares what to do at each stage through lifecycle hooks.

Initialization

Hassette transitions the app through STARTING to RUNNING at startup. All core services (API, Bus, Scheduler, and the internal SQLite telemetry database) are ready before any hook runs.

Three hooks fire in order:

  1. before_initialize
  2. on_initialize, the primary hook where the app subscribes to HA events and schedules recurring tasks
  3. after_initialize
from hassette import App, D
from hassette.models import states


class MyApp(App):
    async def on_initialize(self) -> None:
        await self.bus.on_state_change("sensor.power", handler=self.on_power, name="power_sensor")
        await self.scheduler.run_in(self.check_status, 30, name="startup_check")

    async def on_power(self, new_state: D.StateNew[states.SensorState]) -> None:
        pass

    async def check_status(self) -> None:
        pass

on_initialize is where most apps do their setup. self.bus.on_state_change registers a handler that fires on entity state changes. self.scheduler.run_in schedules a one-shot job after a fixed delay. Both calls are async and must be awaited. By the time on_initialize runs, the bus, scheduler, API, and database are all ready.

before_initialize and after_initialize exist for setup that must happen strictly before or after the main registration. Most apps only need on_initialize.

Note

The base implementations of these hooks are empty. No super() call is necessary.

Shutdown

Hassette transitions the app through STOPPING to STOPPED during shutdown or reload.

Three hooks fire in order:

  1. before_shutdown
  2. on_shutdown
  3. after_shutdown

on_shutdown is for releasing external resources the app allocated directly: open files, raw sockets, or third-party connections. Bus subscriptions, scheduled jobs, and task_bucket tasks are cleaned up automatically.

Automatic Cleanup

After the shutdown hooks complete, Hassette cancels all bus subscriptions created via self.bus, all scheduled jobs created via self.scheduler, and all background tasks tracked by self.task_bucket. Manual unsubscription or job cancellation in on_shutdown is unnecessary.

Warning

initialize, shutdown, and cleanup are marked @final — attempting to override any of them raises CannotOverrideFinalError at class load time. The before_*, on_*, and after_* hooks are the extension points.

Synchronous Lifecycle

AppSync lifecycle hooks

AppSync is for apps that wrap blocking (non-async) third-party libraries. It provides _sync-suffixed variants of each hook. Hassette runs each variant in a thread pool via task_bucket.run_in_thread, so blocking calls do not stall the event loop. The _sync hooks are synchronous and cannot use await.

App (async) AppSync (sync)
before_initialize before_initialize_sync
on_initialize on_initialize_sync
after_initialize after_initialize_sync
before_shutdown before_shutdown_sync
on_shutdown on_shutdown_sync
after_shutdown after_shutdown_sync

The async hooks (on_initialize, on_shutdown, etc.) are marked @final on AppSync and delegate to the _sync variants via the thread pool. Overriding them raises CannotOverrideFinalError at class load time.

The bus, scheduler, and API are async. The .sync facades provide synchronous access from _sync hooks: self.bus.sync, self.scheduler.sync, and self.api.sync.

from hassette import AppSync


class MyApp(AppSync):
    def on_initialize_sync(self) -> None:
        self.bus.sync.on_state_change("light.kitchen", handler=self.on_light_change, name="kitchen")
        self.scheduler.sync.run_in(self.cleanup_task, 60, name="cleanup")

    def on_light_change(self) -> None:
        pass

    def cleanup_task(self) -> None:
        pass

See Also

  • Apps overview: app structure and configuration
  • Task Bucket: background task lifecycle and shutdown behavior
  • Bus: handler registration in on_initialize