TypeRegistry
Home Assistant sends nearly all values as strings over its API — even numbers and booleans. The TypeRegistry converts those strings to the correct Python types (integers, floats, booleans, datetimes, etc.) before they reach your code.
When Do I Need This?
Most apps never need to touch the TypeRegistry. The built-in converters handle all standard Home Assistant types automatically.
You need this page when:
- You have a custom state model whose
value_typeis a type Hassette does not know how to convert (e.g., a third-party type or an enum). - You need to register a converter for a custom extractor in the dependency injection system.
- A built-in conversion is giving unexpected results and you need to understand or override it.
Purpose
Home Assistant's WebSocket API and state system primarily work with string representations of values. For example:
- A temperature sensor might report
"23.5"as a string - A boolean sensor reports
"on"or"off"rather thanTrue/False - Timestamps arrive as ISO 8601 strings
The TypeRegistry automatically converts these string values to their proper Python types, making your code cleaner and more type-safe.
Core Concepts
Implementation details: TypeConverterEntry
Each registered converter is stored as a TypeConverterEntry dataclass containing:
- func: The actual conversion function
- from_type: Source type (e.g.,
str) - to_type: Target type (e.g.,
int) - error_types: Tuple of exception types to catch (defaults to
(ValueError,)) - error_message: Optional custom error message template (uses
{value},{from_type},{to_type}placeholders)
from hassette import TypeConverterEntry
entry = TypeConverterEntry(
func=int,
from_type=str,
to_type=int,
error_message="Cannot convert '{value}' to integer",
)
Registration System
The TypeRegistry provides two ways to register converters:
Decorator Registration
Use @register_type_converter_fn to register a conversion function:
from enum import StrEnum, auto
from typing import Annotated
from hassette import A, App, register_type_converter_fn
class Effect(StrEnum):
BLINK = auto()
BREATHE = auto()
CANDLE = auto()
CHANNEL_CHANGE = auto()
COLORLOOP = auto()
FINISH_EFFECT = auto()
FIREPLACE = auto()
OKAY = auto()
STOP_EFFECT = auto()
STOP_HUE_EFFECT = auto()
@register_type_converter_fn(error_message="'{value}' is not a valid Effect")
def str_to_effect(value: str) -> Effect:
"""Convert string to Effect enum.
Types are inferred from the function signature.
"""
return Effect(value.lower())
# Now you can use it in handlers
class LightEffectApp(App):
async def on_light_effect_change(self, effect: Annotated[Effect, A.get_attr_new("effect")]):
self.logger.info("Light effect: %r", effect)
Simple Type Registration
Use register_simple_type_converter for simple conversions:
from hassette import register_simple_type_converter
# Register a simple converter (uses int() as the converter function)
register_simple_type_converter(
from_type=str,
to_type=int,
fn=int, # Optional - defaults to to_type constructor if not provided
error_message="Cannot convert '{value}' to integer", # Optional
)
Conversion Lookup
The TypeRegistry uses a dictionary with (from_type, to_type) tuples as keys for O(1) lookup performance:
from hassette import TYPE_REGISTRY
# Convert a value
result = TYPE_REGISTRY.convert("42", int) # Returns 42 as int
Integration with State Models
The TypeRegistry works with Hassette's state model system through the value_type ClassVar.
The value_type ClassVar
Each state model class can declare a value_type ClassVar to specify the expected type(s) of the state value:
from typing import ClassVar
from hassette.models.states.base import BaseState
class SensorState(BaseState):
"""State model for sensor entities."""
value_type: ClassVar[type | tuple[type, ...]] = (str, int, float)
The value_type defines what types are valid for the state field. It can be:
- A single type:
value_type = int - A tuple of types:
value_type = (str, int, float) - Defaults to
strif not specified
Automatic Conversion in Models
When a state is created or validated, the BaseState._validate_domain_and_state model validator automatically uses the TypeRegistry to convert the raw state value:
# In BaseState._validate_domain_and_state
values["state"] = TYPE_REGISTRY.convert(state, cls.value_type)
This means when you work with typed state models, values are automatically converted:
from hassette import states
# Raw state data from Home Assistant
raw_data = {
"entity_id": "sensor.temperature",
"state": "23.5", # String from HA
"attributes": {"unit_of_measurement": "°C"},
"context": {"id": "12345", "user_id": "user_1"},
}
# Creating a typed state model automatically converts the value
sensor_state = states.SensorState(**raw_data)
print(type(sensor_state.value)) # <class 'float'> - automatically converted!
Union Type Handling
The TypeRegistry intelligently handles Union types (including value_type tuples) by trying conversions in order:
from typing import Union
# value_type = (int, float, str) becomes Union[int, float, str]
# TypeRegistry tries: str → int, then str → float, then keeps as str
The conversion attempts each type in the Union until one succeeds, preserving the original value if no conversion works.
Integration with Dependency Injection
The TypeRegistry handles automatic type conversion in the dependency injection system, particularly for custom extractors.
Type Conversion in Custom Extractors
When you use Annotated with custom extractors from hassette.event_handling.accessors, the TypeRegistry automatically converts extracted values:
from typing import Annotated
from hassette import A, App, D
class MyExtApp(App):
async def handler(
self,
# Brightness is returned as a string from HA, but TypeRegistry
# automatically converts it to int based on the type hint
brightness: Annotated[int | None, A.get_attr_new("brightness")],
entity_id: D.EntityId,
):
if brightness and brightness > 200:
self.logger.info("%s is very bright: %d", entity_id, brightness)
When a custom extractor returns a value, if the value type doesn't match the annotated type, the TypeRegistry is called to perform the conversion automatically.
Relationship with StateRegistry
The TypeRegistry and StateRegistry work together but serve different purposes:
StateRegistry: Maps Home Assistant domains to Pydantic state model classes
- Purpose: Determines which model class to use for a given entity
- Example:
"sensor.temperature"→SensorStateclass
TypeRegistry: Converts raw values to proper Python types
- Purpose: Ensures state values match expected types
- Example:
"23.5"(string) →23.5(float)
The Workflow
- StateRegistry determines the model class based on domain
- Pydantic validation begins with raw state data
BaseState._validate_domain_and_statechecks thevalue_typeClassVar- TypeRegistry converts the state value to match
value_type - Pydantic continues validation with the properly typed value
Built-in Converters
Hassette includes built-in converters for all standard HA types:
Numeric Conversions
str↔int: Basic integer conversionstr↔float: Floating-point conversionstr→Decimal: High-precision decimal parsingfloat→Decimal: Floating-point to high-precision decimalDecimal→int/float: Precision-loss conversionint→float: Integer to float conversionfloat→int: Float to integer (truncation)
Boolean Conversions
str→bool: Handles Home Assistant boolean strings- True values:
"on","true","yes","1" - False values:
"off","false","no","0" bool→str: Converts to"True"or"False"(Pythonstr()— not HA format)
DateTime Conversions
Uses the whenever library for robust datetime handling:
whenever types:
str→ZonedDateTime: Parse HA datetime strings (ISO, plain, or date-only — assumed system timezone)str→Date: ISO date string viaDate.parse_isostr→Time: ISO time string viaTime.parse_isostr→OffsetDateTime: ISO datetime with UTC offset viaOffsetDateTime.parse_isostr→PlainDateTime: ISO datetime without timezone viaPlainDateTime.parse_isoZonedDateTime→Instant: Strip timezone info (to_instant)ZonedDateTime→PlainDateTime: Drop timezone (to_plain)ZonedDateTime→str: ISO format (format_iso)Time→str: ISO format (format_iso)
Stdlib datetime types:
str→datetime: Parse viaZonedDateTime.py_datetime()str→time: Parse viaTime.parse_iso().py_time()str→date: Parse viaDate.parse_iso().py_date()Time→time: Convert viapy_time()
Conversion Errors
When a conversion fails, the TypeRegistry wraps the error with context:
from hassette import TYPE_REGISTRY
from hassette.exceptions import UnableToConvertValueError
try:
result = TYPE_REGISTRY.convert("not_a_number", int)
except UnableToConvertValueError as e:
print(e) # Error details about the conversion failure
Missing Converters
If no converter is registered for a type pair and the type's constructor also fails, an UnableToConvertValueError is raised:
from hassette import TYPE_REGISTRY
from hassette.exceptions import UnableToConvertValueError
class CustomType:
def __init__(self, value):
# This constructor raises to simulate a type that cannot be built from str
raise TypeError("CustomType cannot be constructed from a string")
try:
result = TYPE_REGISTRY.convert("value", CustomType)
except UnableToConvertValueError as e:
print(e) # "Unable to convert 'value' to <class 'CustomType'>"
Custom Error Messages
Provide helpful error messages in your custom converters:
from hassette import register_type_converter_fn
class MyType:
pass
@register_type_converter_fn(error_message="Cannot convert '{value}' to MyType. Expected format: X,Y,Z")
def str_to_mytype(_: str) -> MyType:
"""Convert string to MyType with clear error handling.
Types inferred from signature: str → MyType
"""
# ... conversion logic with helpful ValueError messages
return MyType()
Inspection and Debugging
Implementation details: inspection API
The TypeRegistry provides methods to inspect registered converters. These are primarily useful for Hassette core developers or for debugging unexpected conversion behavior.
List All Conversions
from hassette import TYPE_REGISTRY
# Get all registered conversions
conversions = TYPE_REGISTRY.list_conversions()
for from_type, to_type, _entry in conversions:
print(f"{from_type.__name__} → {to_type.__name__}")
Output example:
str → int
str → float
str → bool
int → float
...
Check for Specific Converter
from hassette import TYPE_REGISTRY
# Check if a converter exists
key = (str, int)
if key in TYPE_REGISTRY.conversion_map:
entry = TYPE_REGISTRY.conversion_map[key]
print(f"Converter found for {str} -> {int}")
else:
print("No converter registered")
Get Converter Details
from hassette import TYPE_REGISTRY
# Get details about a specific converter
entry = TYPE_REGISTRY.conversion_map.get((str, bool))
if entry:
print(f"Error message: {entry.error_message}")
print(f"Converter: {entry.func}")
Union Type Performance
When converting to Union types, the TypeRegistry tries each type in order until one succeeds:
# For Union[int, float, str]
# 1. Try str → int
# 2. If that fails, try str → float
# 3. If that fails, try str → str (identity)
For better performance with Union types, order the types from most specific to least specific:
- ✅ Good:
Union[int, float, str](tries int first, most specific) - ❌ Less optimal:
Union[str, int, float](str matches everything)
Best Practices
1. Define value_type in State Models
Always specify value_type in custom state models:
from typing import ClassVar
from hassette.models.states import BaseState
class CustomState(BaseState):
# Explicitly define expected types
value_type: ClassVar[type | tuple[type, ...]] = int
2. Use Type Hints with Custom Extractors
Use type hints for automatic conversion in dependency injection:
from typing import Annotated
from hassette import A
# TypeRegistry converts automatically based on type hint
async def handler(
temperature: Annotated[float, A.get_attr_new("temperature")],
humidity: Annotated[int, A.get_attr_new("humidity")],
):
# temperature and humidity are already the correct types
pass
3. Provide Clear Error Messages
When creating custom converters, write helpful error messages:
from hassette import register_type_converter_fn
class MyType:
"""Placeholder for a custom type."""
@register_type_converter_fn(error_message="Cannot convert '{value}' to MyType. Expected format: X,Y,Z")
def str_to_mytype(value: str) -> MyType:
"""Convert string to MyType with clear error handling.
Types inferred from signature: str → MyType
"""
# ... conversion logic with helpful ValueError messages
raise ValueError(f"Cannot parse '{value}' as MyType")
4. Register Converters Early
Register custom converters at module import time using decorators:
# my_converters.py
from hassette import register_type_converter_fn
class MyType:
"""Placeholder for a custom type."""
@register_type_converter_fn # Registered when module is imported
def str_to_mytype(value: str) -> MyType: ...
Then import your converters module in your app's __init__.py or before first use.
5. Test Custom Converters
Always test custom converters with edge cases:
import pytest
from hassette import TYPE_REGISTRY
class RGBColor:
"""Placeholder for a custom RGB color type."""
red: int
green: int
blue: int
def test_custom_converter():
"""Test custom RGB converter."""
# Valid conversion
result = TYPE_REGISTRY.convert("255,128,0", RGBColor)
assert result.red == 255
assert result.green == 128
assert result.blue == 0
# Invalid format
with pytest.raises(ValueError, match="Invalid RGB format"):
TYPE_REGISTRY.convert("not_rgb", RGBColor)
# Out of range
with pytest.raises(ValueError, match="must be between 0 and 255"):
TYPE_REGISTRY.convert("300,128,0", RGBColor)
Common Patterns
Pattern 1: Enum Conversion
Convert Home Assistant string values to Python enums:
from enum import Enum
from hassette import register_type_converter_fn
class FanSpeed(Enum):
LOW = "low"
MEDIUM = "medium"
HIGH = "high"
@register_type_converter_fn
def str_to_fan_speed(value: str) -> FanSpeed:
"""Convert string to FanSpeed enum.
Types inferred from signature: str → FanSpeed
"""
return FanSpeed(value.lower())
Pattern 2: Structured Data
Convert JSON strings to dataclasses:
import json
from dataclasses import dataclass
from hassette import register_type_converter_fn
@dataclass
class DeviceInfo:
name: str
version: str
manufacturer: str
@register_type_converter_fn
def str_to_device_info(value: str) -> DeviceInfo:
"""Parse device info JSON.
Types inferred from signature: str → DeviceInfo
"""
data = json.loads(value)
return DeviceInfo(**data)
Pattern 3: Units of Measurement
Convert strings with units to numeric values:
import re
from hassette import register_type_converter_fn
@register_type_converter_fn
def str_with_units_to_float(value: str) -> float:
"""Extract numeric value from string with units.
Example: '23.5 °C' → 23.5
Types inferred from signature: str → float
"""
match = re.match(r"^([-+]?[0-9]*\.?[0-9]+)", value.strip())
if match:
return float(match.group(1))
raise ValueError(f"Cannot extract number from '{value}'")
See Also
- State Registry - Domain to model class mapping
- Dependency Injection - Using TypeRegistry with custom extractors
- State Models - State model reference