Skip to content

Custom States

Hassette auto-generates typed state classes for standard Home Assistant domains. For custom integrations or third-party add-ons, a custom state class maps an unrecognized domain to a typed Python model. The State Registry — Hassette's internal mapping from domain strings to state classes — picks up the class automatically at definition time via __init_subclass__.

Defining a Custom State

A custom state class inherits from one of Hassette's base state classes. The domain field takes a Literal with the exact domain string from Home Assistant.

from typing import Literal

from hassette.models.states.base import StringBaseState


class MyCustomState(StringBaseState):
    """State class for my_custom_domain entities."""

    domain: Literal["my_custom_domain"]

Registration happens via __init_subclass__, so no explicit call is needed. Each class maps to one domain. Assigning the same Literal value to two classes overwrites the first registration.

Literal["my_custom_domain"] is required. A plain str annotation carries no value at class definition time, so the registry cannot extract the domain name automatically.

Choosing a Base Class

Each base class determines the Python type of value on the resulting state object.

StringBaseState: str value

StringBaseState is the most common choice. It passes through the raw HA state string with no conversion.

from typing import Literal

from hassette.models.states.base import StringBaseState


class LauncherState(StringBaseState):
    domain: Literal["launcher"]

NumericBaseState: numeric value

NumericBaseState converts the raw state string to a numeric type — whole-number strings become int, decimal strings become float. It accepts int, float, and Decimal inputs directly.

from typing import Literal

from hassette.models.states.base import NumericBaseState


class CustomSensorState(NumericBaseState):
    domain: Literal["custom_sensor"]

BoolBaseState: bool value

BoolBaseState converts "on" to True and "off" to False automatically.

from typing import Literal

from hassette.models.states.base import BoolBaseState


class CustomBinaryState(BoolBaseState):
    domain: Literal["custom_binary"]

DateTimeBaseState: ZonedDateTime, PlainDateTime, or Date value

DateTimeBaseState parses the raw state string into a whenever datetime type (from whenever import ZonedDateTime — Hassette's date/time library). The exact type depends on the string format from Home Assistant.

from typing import Literal

from hassette.models.states.base import DateTimeBaseState


class TimestampState(DateTimeBaseState):
    domain: Literal["timestamp"]

TimeBaseState: Time value

TimeBaseState parses the raw state string into a whenever.Time value.

from typing import Literal

from hassette.models.states.base import TimeBaseState


class TimeOnlyState(TimeBaseState):
    domain: Literal["time_only"]

Custom value type: inherit BaseState directly

When no built-in base class fits, a class can inherit from BaseState[T] directly. The value_type class variable declares the accepted types. Hassette validates state values against value_type at runtime.

from enum import StrEnum
from typing import Any, ClassVar, Literal

from hassette.models.states.base import BaseState


class MyValueType(StrEnum):
    OPTION_A = "option_a"
    OPTION_B = "option_b"
    OPTION_C = "option_c"


class MyCustomState(BaseState[MyValueType]):
    domain: Literal["my_custom_domain"]

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

value_type should include type(None) when the state can be unset.

Adding Typed Attributes

Domain-specific attributes beyond value belong in an attributes class that inherits from AttributesBase — a Pydantic model subclass where fields map to HA attribute keys by name. The attributes field on the state class accepts this class, overriding the default.

from typing import Literal

from pydantic import Field

from hassette.models.states.base import AttributesBase, StringBaseState


class RedditAttributes(AttributesBase):
    """Attributes for Reddit entities."""

    subreddit: str | None = Field(default=None)
    post_count: int | None = Field(default=None)
    karma: int | None = Field(default=None)


class RedditState(StringBaseState):
    """State class for reddit domain entities."""

    domain: Literal["reddit"]
    attributes: RedditAttributes  # Override attributes type

Fields on the attributes class are optional by default when typed with | None. Hassette passes through any undeclared attribute keys. They remain accessible via state.attributes.extras.

Using Custom States in Apps

Via self.states[CustomStateClass]

self.states[RedditState] returns a DomainStates collection typed to RedditState. Iteration yields (entity_id, state) pairs where each state is a fully converted RedditState instance.

from hassette import App

from .my_states import RedditState  # pyright: ignore[reportMissingImports]


class MyApp(App):
    async def on_initialize(self):
        # Get all reddit entities
        reddit_states = self.states[RedditState]

        for entity_id, state in reddit_states:
            print(f"{entity_id}: {state.value}")
            if state.attributes.karma:
                print(f"  Karma: {state.attributes.karma}")

With Dependency Injection

D.StateNew[RedditState] in a handler parameter tells Hassette to convert the incoming event's new state to a RedditState before calling the handler. Dependency Injection covers the full parameter reference.

from typing import Annotated

from hassette import A, App, D

from .my_states import RedditState  # pyright: ignore[reportMissingImports]


class MyApp(App):
    async def on_initialize(self):
        await self.bus.on_state_change("reddit.my_account", handler=self.on_reddit_change, name="reddit_account")

    async def on_reddit_change(self, new_state: D.StateNew[RedditState], karma: Annotated[int | None, A.get_attr_new("karma")]):
        self.logger.info("New karma: %d", karma or 0)

Troubleshooting

Class not registering. The domain field must use Literal["domain_name"], not str. A plain str annotation gives the registry no value to register at class creation time. If __init_subclass__ is overridden, it must call super().__init_subclass__() so registration still runs.

Type hints not working. Property-style access (self.states.my_domain) is only available for domains declared in Hassette's .pyi stub. Custom domains always use self.states[CustomStateClass] for full type checking.

Conversion fails. The base class must match the entity's actual value type in Home Assistant. The raw state data is visible via hassette log --app <key> or the HA developer tools, which confirms the format before a base class is selected.

See Also