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
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
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.
value is typed Python, not the raw HA string
Home Assistant stores "on"/"off" strings; state conversion turns them into True/False for toggle domains like light, switch, and binary_sensor. state.value == "on" is always False — compare against True instead. Code ported from AppDaemon or HA templates that compares against "on" silently never matches. The changed_to=/changed_from= filters on on_state_change() are the exception: they compare raw HA strings.
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").
context holds the HA event context that produced this state: context.id, context.parent_id, and context.user_id. It traces which automation or user triggered the change.
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
# e.g. states.LightState, states.SunState, states.BinarySensorState
Three common examples:
states.LightStatehasvalue: bool | None,attributes.brightness: int | None,attributes.color_temp_kelvin: int | Nonestates.SensorStatehasvalue: str | None,attributes.unit_of_measurement: str | None,attributes.device_class: str | Nonestates.BinarySensorStatehasvalue: 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:
# battery_level is not declared on the typed attributes model,
# so read it via .extra(), which returns None when absent
battery = sensor.attributes.extra("battery_level")
if battery is not None and battery < 20:
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.
StateManager itself is also iterable: self.states.items() yields (key, DomainStates) pairs for every registered state class, and MyState in self.states checks whether a class is registered. Useful for diagnostics and generic helpers that sweep all domains.
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
- Subscription Methods:
on_state_change,on_attribute_change, and their parameters - Custom States: define typed models for custom integrations
- State Conversion: how raw HA dicts become typed Python objects
- API Methods: retrieve states via the REST/WebSocket API
- App Cache: persist data locally across restarts