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
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
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
- State Conversion: how automatic registration works, domain overrides, and custom value converters
- Dependency Injection: injecting typed states into event handlers