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.
A conversion turns the raw state dict HA returns into a typed state object. When a conversion fails, behavior depends on the access style. The third row uses direct access, covered in Direct Entity Access below.
| Access style | Missing entity | Conversion failure |
|---|---|---|
self.states.light["kitchen"] |
raises KeyError |
raises UnableToConvertStateError |
self.states.light.get("kitchen") |
returns None |
raises UnableToConvertStateError |
self.states.get("light.kitchen") |
returns None |
returns None |
UnableToConvertStateError (from hassette.exceptions) carries entity_id and state_class fields. They identify which entity and target type failed. The error signals a shape mismatch between the HA state dict and the domain model — for example, an int attribute arriving as a string that cannot be coerced.
Domain iteration (for entity_id, state in self.states.light.items()) skips un-convertible entities and logs the error. Valid entities still flow through the iterator.
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.
time_since_last_change, time_since_last_update, time_since_last_report return TimeDelta | None — the elapsed time since each corresponding timestamp, or None when the timestamp itself is absent. Useful for checks like "has this entity been in its current state for more than 10 minutes?" without manual arithmetic.
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 implements collections.abc.Mapping — for entity_id in self.states.sensor yields entity ID strings, matching Python's dict convention. .items() yields (entity_id, state) pairs. .keys(), .values(), and .items() return re-iterable views that support len() and in. 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.items():
# 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)
.keys(), .values(), and .items() views are lazy per iteration — each for loop parses raw HA state dicts into typed objects on demand. .to_dict() is the one eager method, parsing 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.
Presence
Presence is one of the most common conditions in home automation. StateManager answers it directly, reading the person domain from the local cache — synchronous, no await, no API call.
from hassette import App
class PresenceApp(App):
async def on_initialize(self):
# Quantifiers read the person domain (device_tracker fallback) from the
# local cache — synchronous, no await, no API call.
if self.states.nobody_home():
await self.api.turn_off("climate.house", domain="climate")
if self.states.anybody_home():
await self.api.turn_on("light.porch", domain="light")
if self.states.everybody_home():
self.logger.info("The whole household is home")
# is_home checks a single entity by full entity ID.
if self.states.is_home("person.jessica"):
await self.api.turn_on("light.office", domain="light")
Three quantifiers cover the household, and is_home covers one entity:
anybody_home()—Trueif at least one tracked person is home.everybody_home()—Trueif every tracked person is home.Falsewhen no presence entities are tracked.nobody_home()—Trueif no tracked person is home. The inverse ofanybody_home().is_home(entity)—Trueif a singleperson.*ordevice_tracker.*entity is home. Takes a full entity ID.
The quantifiers read the person domain, falling back to device_tracker only when no person entities are configured. person entities aggregate a real person's devices, so they answer "is this person home?" more reliably than a single device tracker.
"home" is the Home Assistant state both domains report when an entity is in the home zone; anything else ("not_home", a named zone like "Work") counts as away.
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