Skip to content

State Registry

The StateRegistry maps Home Assistant domains (like light, sensor, switch) to Pydantic state model classes, so raw dictionaries from HA become typed Python objects automatically. If you haven't defined a custom state class yet, that page covers the basics of creating one.

When Do I Need This?

Most apps never need to touch the StateRegistry directly. The built-in state classes cover all standard Home Assistant domains, and the DI system and self.states cache use the registry automatically.

You need this page when:

  • You are writing a custom state class for a domain Hassette does not yet know about.
  • You want to override the default state class for an existing domain (e.g., to add custom attributes).
  • You are seeing unexpected state types at runtime and need to understand how the mapping works.

What is the State Registry?

When Home Assistant sends state change events, the state data arrives as untyped dictionaries. The StateRegistry converts these dictionaries into typed Pydantic models based on the entity's domain:

# Raw data from Home Assistant (untyped dict)
raw_data = {
    "entity_id": "light.bedroom",
    "state": "on",
    "attributes": {"brightness": 200, "color_temp": 370},
}

# After StateRegistry conversion (typed model)
# LightState(
#     entity_id="light.bedroom",
#     state="on",
#     attributes=LightAttributes(brightness=200, color_temp=370),
# )

How It Works

Automatic Registration

All classes that inherit from BaseState — the root model class that all Hassette state types extend — are registered automatically at class creation time if they have a valid domain. You do not need to call any registration function — defining the class is sufficient.

Implementation details: __init_subclass__ hook

Registration happens via the __init_subclass__ hook in BaseState, which adds the class to the global StateRegistry as soon as the class body is evaluated.

from typing import ClassVar

from hassette.models.states import BaseState


class LightAttributes(BaseState):  # simplified for example
    pass


class LightState(BaseState):
    """State model for light entities."""

    domain: ClassVar[str] = "light"
    attributes: LightAttributes

Domain Lookup

When you need to convert state data, the registry provides lookup functions:

from hassette import STATE_REGISTRY

# Get class for a domain
state_class = STATE_REGISTRY.resolve(domain="light")
# Returns: LightState

Relationship with TypeRegistry

The StateRegistry and TypeRegistry handle different parts of type conversion for Home Assistant state data:

  • StateRegistry → Determines which state model class to use based on domain
  • TypeRegistry → Converts raw values to proper Python types during model validation

The Complete Flow

When state data arrives from Home Assistant, both registries cooperate:

  1. Raw data arrives from Home Assistant:

    state_dict = {
        "entity_id": "time.current",
        "state": "12:01:01",  # String from HA
    }
    

  2. StateRegistry determines the model class based on the time domain → returns TimeState

  3. Pydantic validation begins on the TimeState model

  4. BaseState._validate_domain_and_state checks the value_type ClassVar

  5. TypeRegistry converts "12:01:01" (str) → whenever.Time

  6. Validation completes with the properly typed value:

    from hassette import STATE_REGISTRY
    
    state_dict = {
        "entity_id": "time.current",
        "state": "12:01:01",
    }
    time_state = STATE_REGISTRY.try_convert_state(state_dict)
    # Result: TimeState with state=whenever.Time
    

The value_type ClassVar

State model classes use the value_type ClassVar to declare expected state value types:

from typing import Any, ClassVar, Literal

from whenever import Time

from hassette.models.states import BaseState


class TimeBaseState(BaseState[Time | None]):
    """Base class for Time states.

    Valid state values are Time or None.
    """

    value_type: ClassVar[type[Any] | tuple[type[Any], ...]] = (Time, type(None))


class TimeState(TimeBaseState):
    """Representation of a Home Assistant time state.

    See: https://www.home-assistant.io/integrations/time/
    """

    domain: Literal["time"]

During validation, if the raw state value doesn't match value_type, the TypeRegistry automatically converts it.

This means when you work with state models, numeric values, booleans, and datetimes are automatically the correct Python type, not strings.

Why Two Registries?

Each registry answers a different question:

  • StateRegistry: "What model class?" (domain → model mapping)
  • TypeRegistry: "What type?" (value → type conversion)

Splitting them means the TypeRegistry can be reused throughout the framework (DI system, custom extractors) and you can extend either one without touching the other.

Example Benefits:

from typing import Annotated

from hassette import A


# TypeRegistry also works in dependency injection
# and converts attribute values too
async def handler(brightness: Annotated[int, A.get_attr_new("brightness")]):
    # brightness is int, not string, thanks to TypeRegistry
    pass

See TypeRegistry for more details on automatic value conversion.

State Conversion

The primary use of the StateRegistry is converting raw state dictionaries to typed models:

Direct Conversion

from hassette import STATE_REGISTRY

# Raw state data from Home Assistant
state_dict = {
    "entity_id": "light.bedroom",
    "state": "on",
    "attributes": {"brightness": 200},
    # ... more fields
}

# Convert to typed model
light_state = STATE_REGISTRY.try_convert_state(state_dict)
# Returns: LightState instance

The try_convert_state method:

  • Extracts the domain from the entity_id
  • Looks up the corresponding state class
  • Converts the dictionary to a Pydantic model instance
  • Falls back to BaseState for unknown domains

Via Dependency Injection

The StateRegistry works with dependency injection automatically:

from hassette import App, D, states


class MyApp(App):
    async def on_light_change(self, new_state: D.StateNew[states.LightState]):
        # new_state is already a LightState instance
        pass

Behind the scenes, the DI system uses convert_state_dict_to_model() which calls the StateRegistry.

Domain Override

If you want to override the default state class for a domain (for example, to add custom attributes), define your class after imports:

from typing import ClassVar

from hassette.models.states import SensorAttributes, SensorState


class CustomSensorAttributes(SensorAttributes):
    custom_field: str | None = None


class CustomSensorState(SensorState):
    """Extended sensor state with custom attributes."""

    domain: ClassVar[str] = "sensor"
    attributes: CustomSensorAttributes

The StateRegistry silently replaces the existing class with your custom one.

Union Type Support

The StateRegistry works with Union types, automatically selecting the correct state class:

from hassette import App, D, states


class SensorApp(App):
    async def on_sensor_change(self, new_state: D.StateNew[states.SensorState | states.BinarySensorState]):
        # StateRegistry determines the correct type based on domain
        if new_state.domain == "sensor" and new_state.value:
            # new_state is SensorState
            float(new_state.value)
        else:
            # new_state is BinarySensorState
            pass

The conversion logic: 1. Extracts the domain from the entity_id 2. Checks each type in the Union 3. Uses the state class whose domain matches 4. Falls back to BaseState if no match

Error Handling

The StateRegistry raises specific exceptions for different error conditions:

InvalidDataForStateConversionError

Raised when state data is malformed or missing required fields:

from hassette import STATE_REGISTRY
from hassette.exceptions import InvalidDataForStateConversionError

try:
    state = STATE_REGISTRY.try_convert_state(None)  # Invalid data
except InvalidDataForStateConversionError as e:
    print(f"Invalid state data: {e}")

InvalidEntityIdError

Raised when the entity_id format is invalid:

from hassette import STATE_REGISTRY
from hassette.exceptions import InvalidEntityIdError

try:
    # Entity ID must have format "domain.entity"
    state = STATE_REGISTRY.try_convert_state({"entity_id": "invalid"})
except InvalidEntityIdError as e:
    print(f"Invalid entity ID: {e}")

UnableToConvertStateError

Raised when conversion to the target state class fails:

from hassette import STATE_REGISTRY
from hassette.exceptions import UnableToConvertStateError

data = {"entity_id": "light.bedroom", "state": "on"}  # Simplified data
try:
    state = STATE_REGISTRY.try_convert_state(data)
except UnableToConvertStateError as e:
    print(f"Conversion failed: {e}")
    # Falls back to BaseState or re-raises depending on context

Integration with Other Components

With Dependency Injection

The StateRegistry powers all state type conversions in dependency injection:

from hassette import D, states

# DI annotation uses StateRegistry internally
new_state: D.StateNew[states.LightState]

With States Resource

The States cache uses the StateRegistry for all state lookups:

from hassette import App


class StatesUsage(App):
    async def usage(self):
        # Returns typed LightState instance
        light = self.states.light.get("light.bedroom")
        self.logger.info(light)

Advanced Usage

Accessing the Registry

The StateRegistry can be imported from Hassette directly:

from hassette import STATE_REGISTRY

registry = STATE_REGISTRY

In apps, you don't need direct access — the DI system and API methods handle conversions for you.

If you do need to access it, it is accessible through self.hassette.state_registry.

See Also