Skip to content

Your First Automation

This page adds two features to the app from the Quickstart:

  • A sunset handler that turns on a light when the sun sets, with typed state data filled in automatically
  • A heartbeat job that logs every minute

Subscribe to a State Change

from hassette import App, AppConfig, D, states


class MyAppConfig(AppConfig):
    greeting: str = "Hello from Hassette!"


class MyApp(App[MyAppConfig]):
    async def on_initialize(self):
        self.logger.info(self.app_config.greeting)
        await self.bus.on_state_change(
            "sun.*", handler=self.on_sun_change, name="sun_change"
        )

    async def on_sun_change(self, new_state: D.StateNew[states.SunState]):
        self.logger.info("Sun changed: %s", new_state.value)

        if new_state.value == "below_horizon":
            await self.api.turn_on("light.porch", domain="light")
            self.logger.info("Porch light turned on")

self.bus.on_state_change() registers a handler that fires whenever an entity's state changes, like a light switching on or a sensor reporting a new reading. "sun.*" is a glob pattern that matches any entity in the sun domain. In practice, that's sun.sun. The name= parameter labels this handler in logs and the web UI (a monitoring dashboard Hassette includes).

The handler parameter new_state: D.StateNew[states.SunState] is a type annotation that tells Hassette: when this handler fires, pass in the new state as a SunState object. The bracket syntax works like list[str] in standard Python generics. D.StateNew[T] means "a new-state value, typed as T." Here is what the two imports do:

  • D is hassette.dependencies, a module of type annotations. D.StateNew[T] means "give me the new state, converted to type T."
  • states is hassette.models.states, typed state classes for each HA domain. states.SunState has a .value attribute holding "above_horizon" or "below_horizon".

This is a Hassette feature. Standard Python ignores type annotations at runtime, but Hassette inspects them to know what to pass into each handler. No event dict parsing needed. Your IDE knows the type, and Pyright (a Python type checker, optional but useful in VS Code) catches typos.

self.api.turn_on() calls the light.turn_on service in Home Assistant, the same action as toggling a light from the HA UI. The domain="light" parameter tells HA which service domain to use. You can find available services in Developer Tools → Services in your Home Assistant instance.

Schedule a Recurring Job

from hassette import App, AppConfig, D, states


class MyAppConfig(AppConfig):
    greeting: str = "Hello from Hassette!"


class MyApp(App[MyAppConfig]):
    async def on_initialize(self):
        self.logger.info(self.app_config.greeting)
        await self.bus.on_state_change(
            "sun.*", handler=self.on_sun_change, name="sun_change"
        )
        await self.scheduler.run_minutely(self.log_heartbeat)

    async def on_sun_change(self, new_state: D.StateNew[states.SunState]):
        self.logger.info("Sun changed: %s", new_state.value)

        if new_state.value == "below_horizon":
            await self.api.turn_on("light.porch", domain="light")
            self.logger.info("Porch light turned on")

    async def log_heartbeat(self):
        self.logger.info("Heartbeat")

self.scheduler.run_minutely() runs log_heartbeat every minute. The first run fires one minute after startup. Hassette tracks the job and cancels it automatically on shutdown.

log_heartbeat has no D.* annotations in its signature. Not every handler needs them. See Scheduler Methods for run_daily, run_cron, run_once, and more.

Run It

Replace your apps/main.py with the complete app from the previous snippet. Stop Hassette with Ctrl+C and run hassette run -e .env again. You see new log lines:

INFO hassette.MyApp.0 — Hello from Hassette!
INFO hassette.MyApp.0 — Heartbeat

The Sun changed and Porch light turned on lines appear at the next sunset. To test the handler now without waiting, trigger a state change manually:

  1. In Home Assistant, go to Settings → Developer Tools.
  2. Go to the States tab and find sun.sun.
  3. Change the state value to below_horizon and click Set State.

The handler fires within milliseconds. You see Sun changed: below_horizon and Porch light turned on in the logs.

Next Steps

  • Bus & Handlers: attribute changes, service calls, glob patterns, predicates, and conditions
  • Dependency Injection: all the types you can extract into handler parameters
  • Scheduler Methods: run_daily, run_cron, run_once, and jitter
  • Testing Your Apps: unit tests using AppTestHarness
  • Recipes: complete worked examples for motion lights, presence detection, and more
  • Docker: run Hassette in production as a container