Skip to content

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_type is 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 than True/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 str if 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"SensorState class

TypeRegistry: Converts raw values to proper Python types

  • Purpose: Ensures state values match expected types
  • Example: "23.5" (string) → 23.5 (float)

The Workflow

  1. StateRegistry determines the model class based on domain
  2. Pydantic validation begins with raw state data
  3. BaseState._validate_domain_and_state checks the value_type ClassVar
  4. TypeRegistry converts the state value to match value_type
  5. Pydantic continues validation with the properly typed value

Built-in Converters

Hassette includes built-in converters for all standard HA types:

Numeric Conversions

  • strint: Basic integer conversion
  • strfloat: Floating-point conversion
  • strDecimal: High-precision decimal parsing
  • floatDecimal: Floating-point to high-precision decimal
  • Decimalint / float: Precision-loss conversion
  • intfloat: Integer to float conversion
  • floatint: Float to integer (truncation)

Boolean Conversions

  • strbool: Handles Home Assistant boolean strings
  • True values: "on", "true", "yes", "1"
  • False values: "off", "false", "no", "0"
  • boolstr: Converts to "True" or "False" (Python str() — not HA format)

DateTime Conversions

Uses the whenever library for robust datetime handling:

whenever types:

  • strZonedDateTime: Parse HA datetime strings (ISO, plain, or date-only — assumed system timezone)
  • strDate: ISO date string via Date.parse_iso
  • strTime: ISO time string via Time.parse_iso
  • strOffsetDateTime: ISO datetime with UTC offset via OffsetDateTime.parse_iso
  • strPlainDateTime: ISO datetime without timezone via PlainDateTime.parse_iso
  • ZonedDateTimeInstant: Strip timezone info (to_instant)
  • ZonedDateTimePlainDateTime: Drop timezone (to_plain)
  • ZonedDateTimestr: ISO format (format_iso)
  • Timestr: ISO format (format_iso)

Stdlib datetime types:

  • strdatetime: Parse via ZonedDateTime.py_datetime()
  • strtime: Parse via Time.parse_iso().py_time()
  • strdate: Parse via Date.parse_iso().py_date()
  • Timetime: Convert via py_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