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:
-
Raw data arrives from Home Assistant:
state_dict = { "entity_id": "time.current", "state": "12:01:01", # String from HA } -
StateRegistry determines the model class based on the
timedomain → returnsTimeState -
Pydantic validation begins on the
TimeStatemodel -
BaseState._validate_domain_and_state checks the
value_typeClassVar -
TypeRegistry converts
"12:01:01"(str) →whenever.Time -
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
BaseStatefor 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
- Type Registry - automatic value type conversion
- Dependency Injection - using StateRegistry via DI annotations
- Custom States - defining your own state classes