Skip to content

Dependency Injection

Hassette's dependency injection system extracts typed data from events and passes it to handler parameters. Like FastAPI's Depends(), Hassette resolves handler parameters at call time — but instead of a dependency function, type annotations from the D module declare what to extract. All annotations live in hassette.event_handling.dependencies, imported as D: from hassette import D.

from hassette import App, D, states


class LightMonitor(App):
    async def on_initialize(self):
        await self.bus.on_state_change(
            "light.bedroom",
            handler=self.on_light_change,
            name="bedroom_light",
        )

    async def on_light_change(
        self,
        new_state: D.StateNew[states.LightState],
        entity_id: D.EntityId,
    ):
        brightness = new_state.attributes.brightness
        self.logger.info("%s brightness: %s", entity_id, brightness)

D.StateNew[states.LightState] extracts the new state and converts it to a typed LightState. D.EntityId extracts the entity ID as a string. The handler receives clean data with no event parsing.

Annotation Reference

State Extractors

State extractors resolve typed state objects from state change events. T is any state class from hassette.models.states — a full list is at State Conversion.

Annotation Returns If missing
D.StateNew[T] T Handler skipped
D.StateOld[T] T Handler skipped
D.MaybeStateNew[T] T \| None None
D.MaybeStateOld[T] T \| None None
from hassette import App, D, states


class TempApp(App):
    async def on_temp_change(
        self,
        new: D.StateNew[states.SensorState],
        old: D.MaybeStateOld[states.SensorState],
    ):
        if old and old.value and new.value:
            delta = float(new.value) - float(old.value)
            self.logger.info(
                "Temperature moved %.1f°F", delta
            )

When a required extractor finds no value, Hassette skips the handler invocation and logs the failure at ERROR level — no exception propagates to your app. MaybeStateOld returns None on the first event for a new entity with no previous state (typically on startup or when an entity first appears).

Identity Extractors

Identity extractors resolve entity IDs and domains from events.

Annotation Returns If missing
D.EntityId str Handler skipped
D.MaybeEntityId str \| MISSING_VALUE Falsy sentinel
D.Domain str Handler skipped
D.MaybeDomain str \| MISSING_VALUE Falsy sentinel
from hassette import App, D


class LightApp(App):
    async def on_any_light(self, entity_id: D.EntityId, domain: D.Domain):
        self.logger.info("Light entity %s in domain %s changed", entity_id, domain)

MISSING_VALUE is a falsy sentinel from hassette.const indicating a field does not exist on the event. It is not NoneNone means the field exists with a null value. Testing with if entity_id: covers both the present and absent cases. D.MaybeEntityId is useful in generic handlers registered via on() where the event may not have an entity_id field.

Other Extractors

Annotation Returns If missing Use case
D.EventData[T] T Handler skipped Typed payload from Bus.emit broadcast events
D.EventContext HassContext Handler skipped Home Assistant event context (user ID, parent/origin IDs)
D.TypedStateChangeEvent[T] TypedStateChangeEvent[T] Always present Full event with both old and new states typed

D.EventData[T] pairs with Bus.emit for cross-app communication — one app sends a typed payload, and other apps subscribe to receive it. The emitting app sends a dataclass; the receiving handler annotates its parameter with the same type:

from dataclasses import dataclass

from hassette import App, AppConfig, D


@dataclass(frozen=True, slots=True)
class SensorAlert:
    sensor_id: str
    reading: float


class AlertApp(App[AppConfig]):
    async def on_initialize(self) -> None:
        await self.bus.on(topic="sensor.alert", handler=self.on_alert, name="alert_handler")

    async def on_alert(self, alert: D.EventData[SensorAlert]) -> None:
        self.logger.warning("Sensor %s reading: %.1f", alert.sensor_id, alert.reading)

Combining Annotations

Handlers accept multiple DI parameters. Hassette resolves each independently from the same event.

from hassette import App, D, states


class ClimateApp(App):
    async def on_climate_change(
        self,
        new: D.StateNew[states.ClimateState],
        entity_id: D.EntityId,
        context: D.EventContext,
    ):
        temp = new.attributes.current_temperature
        self.logger.info(
            "%s temperature: %s (user: %s)",
            entity_id,
            temp,
            context.user_id or "system",
        )

Union Types

State extractors accept union types for handlers that cover multiple entity domains.

from hassette import App, D, states


class SensorApp(App):
    async def on_sensor_change(
        self,
        new: D.StateNew[
            states.SensorState | states.BinarySensorState
        ],
        entity_id: D.EntityId,
    ):
        if isinstance(new, states.SensorState):
            self.logger.info("Sensor %s: %s", entity_id, new.value)
        else:
            self.logger.info("Binary %s: %s", entity_id, new.value)

Hassette determines the concrete state class from the entity's domain at dispatch time — see State Conversion for details.

Custom Keyword Arguments

DI composes with kwargs= passed at registration. DI-annotated parameters resolve from the event; remaining keyword arguments pass through unchanged from the registration call.

from hassette import App, D, states


class TempApp(App):
    async def on_initialize(self):
        await self.bus.on_state_change(
            "sensor.temperature",
            handler=self.on_temp_change,
            kwargs={"threshold": 75.0},
            name="temp_threshold",
        )

    async def on_temp_change(
        self,
        new: D.StateNew[states.SensorState],
        entity_id: D.EntityId,
        threshold: float,
    ):
        temp = float(new.value) if new.value else 0.0
        if temp > threshold:
            self.logger.warning(
                "%s is %.1f°F (threshold: %.1f)",
                entity_id,
                temp,
                threshold,
            )

Handler Signature Restrictions

Any handler with at least one D.* annotation is a DI handler. DI handlers do not support positional-only parameters (those before /) or *args. Regular parameters and **kwargs are both valid. Every DI parameter requires a type annotation. Hassette uses the annotation to determine what to extract.

Not all D.* annotations work with every subscription method. Subscription Methods lists the compatible annotations for each method.

See Also