Skip to content

States

The StateManager keeps a real-time, in-memory copy of all Home Assistant entity states. self.states is a StateManager instance available on every App — it provides synchronous, typed access with no await and no API calls.

flowchart TD
    subgraph ha["Home Assistant"]
        HA["State change events"]
    end

    subgraph framework["Framework"]
        WS["WebsocketService"]
        SP["StateProxy<br/><i>in-memory cache</i>"]
        WS --> SP
    end

    subgraph app["App"]
        SM["self.states<br/><i>typed, sync access</i>"]
    end

    HA -- "WebSocket" --> WS
    SP --> SM

    style ha fill:#f0f0f0,stroke:#999
    style framework fill:#fff0e8,stroke:#cc8844
    style app fill:#e8f0ff,stroke:#6688cc
Hold "Ctrl" to enable pan & zoom

Reading State

Domain Access

self.states.light, self.states.sensor, and similar domain properties return a DomainStates collection — a dict-like view keyed by entity name, typed to that domain's state class.

from hassette import App


class StateApp(App):
    async def on_initialize(self):
        # Access by domain
        light = self.states.light.get("light.kitchen")

        # Access attributes safely
        if light:
            self.logger.info("Brightness: %s", light.attributes.brightness)

        # if you know the entity exists you can access it
        # directly using dictionary-style access
        self.states.sensor["temperature"]

The short entity name omits the domain prefix. self.states.light.get("kitchen") and self.states.light.get("light.kitchen") resolve to the same entity.

.get() returns None for missing entities. Bracket access raises KeyError.

Direct Entity Access

self.states.get(entity_id) accepts a full entity ID and resolves to the most specific built-in type for that domain. LightState for light.*, SensorState for sensor.*, BaseState for any domain without a built-in class.

from hassette import App


class DirectAccessApp(App):
    async def on_initialize(self):
        # Access any entity by full entity ID
        light = self.states.get("light.kitchen")
        if light:
            self.logger.info("State: %s", light.value)

        # Works for any domain, even unregistered ones
        custom = self.states.get("my_domain.some_entity")
        if custom:
            self.logger.info("Domain: %s, Value: %s", custom.domain, custom.value)

Generic Access

self.states[CustomState] returns a DomainStates collection typed to a custom state class. This pattern covers custom integrations and third-party add-ons whose domain has no built-in class.

from my_app import MyCustomState  # pyright: ignore[reportMissingImports]

from hassette import App


class GenericApp(App):
    async def on_initialize(self):
        # dictionary like access with state class
        my_instance = self.states[MyCustomState].get("work")
        if my_instance:
            self.logger.info("MyCustomState value: %s", my_instance.value)

Custom state class definition and registration are covered in Custom States.

What a State Object Contains

Every state object is a BaseState subclass. The following fields and properties are available on all of them.

value is the entity's current state, typed for the domain. SwitchState.value is bool | None, SensorState.value is str | None, SelectState.value is str | None. When HA reports "unknown" or "unavailable", value is None. is_unknown and is_unavailable identify which case applies.

attributes is a typed AttributesBase subclass with domain-specific fields. LightState.attributes.brightness is an integer. ClimateState.attributes.current_temperature is a float. Pyright knows the types.

is_unknown and is_unavailable are True when HA reports the entity as "unknown" or "unavailable", respectively. Both flags are False for normal states.

is_group is True when the entity is a group. For group entities, the entity_id attribute holds a list of member entity IDs rather than the group's own ID.

extras and extra(key, default=None) access untyped state fields not declared on the BaseState model. Typed attributes cover the common cases; these handle the rest.

last_changed, last_updated, last_reported are ZonedDateTime | None timestamps from HA. ZonedDateTime is from the whenever library, which Hassette uses for all date/time operations — it behaves like a timezone-aware datetime and converts via .to_stdlib() when a library requires it. last_changed updates only when the state string changes. last_updated updates when state or attributes change. last_reported updates on every write.

entity_id and domain hold the full entity ID ("light.kitchen") and its domain ("light").

Attribute Helpers

AttributesBase exposes two helpers for attributes not declared on the typed model.

attributes.extras returns a dict[str, Any] of undeclared fields. attributes.extra(key, default=None) fetches a single undeclared field with a fallback.

attributes.has_feature(flag) tests a bit in supported_features. Each domain defines its own IntFlag enum for feature constants. LightEntityFeature has EFFECT, FLASH, and TRANSITION.

Built-in State Types

Hassette auto-generates typed state classes for 55 Home Assistant domains from HA core source. All classes are available from the states module:

from hassette import states  # pyright: ignore[reportUnusedImport]

# e.g. states.LightState, states.SunState, states.BinarySensorState

Three common examples:

  • states.LightState has value: bool | None, attributes.brightness: int | None, attributes.color_temp_kelvin: int | None
  • states.SensorState has value: str | None, attributes.unit_of_measurement: str | None, attributes.device_class: str | None
  • states.BinarySensorState has value: bool | None, attributes.device_class: str | None

The API reference lists all 55 classes with their full attribute signatures. Domains not covered there are handled by Custom States.

Iterating Over States

DomainStates supports direct iteration over (entity_id, state) pairs — for entity_id, state in self.states.sensor yields tuples, unlike a plain dict which yields keys. .keys(), .values(), .to_dict(), containment checks ("kitchen" in self.states.light), and len() also work.

from hassette import App


class IteratorApp(App):
    async def on_initialize(self):
        # Find all low battery sensors
        for entity_id, sensor in self.states.sensor:
            # sensor.attributes is a plain Pydantic model; unrecognised fields are
            # not declared on the class, so access them via hasattr/getattr.
            if not hasattr(sensor.attributes, "battery_level"):
                continue
            if sensor.attributes.battery_level < 20:  # pyright: ignore[reportAttributeAccessIssue]
                self.logger.warning("Low battery: %s", entity_id)

.items(), .iterkeys(), and .itervalues() are lazy — they parse raw HA state dicts into typed objects on demand. .keys(), .values(), and .to_dict() are eager and parse all entities up front. Lazy iteration performs better for large domains like sensor.

Good to Know

Startup. The cache is populated at startup via a full API fetch before on_initialize runs. Apps can read current state immediately.

Staleness. WebSocket state_changed events keep the cache current. A periodic background poll (default every 30 seconds) guards against missed events. The StateManager event handler runs before app handlers, so handlers always see the latest state.

Reconnection. During a HA disconnect the cache is retained — self.states.get() returns the last known (stale) values while Hassette reconnects. Once the reconnect completes, a fresh API fetch replaces the cache atomically.

Missing entities. .get() returns None for absent entities. Bracket access raises KeyError. .get() with a None check is the safe path when entity presence is uncertain.

See Also