States
Hassette maintains a local, real-time cache of all Home Assistant states. This is available as an instance of StateManager, accessible via self.states in your apps
Diagram
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["Your 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
Using the StateManager
Use self.states instead of API calls to read entity states. It gives you:
- Speed: Instant access from local memory.
- Simplicity: Synchronous access without
await. - Efficiency: No network overhead or rate limiting concerns.
- Consistency: Event-driven updates ensure your app sees the latest state changes.
- The StateManager event handler is prioritized over app event handlers to ensure you always have a consistent view of the latest states.
Domain Access
The easiest way to access states is via domain properties.
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"]
Notice how you do not need to use the domain in the entity ID - since you're already accessing the domain via self.states.sensor, you only need to provide the entity name.
Bracket access raises KeyError for missing entities
self.states.light["bedroom"] raises KeyError — not EntityNotFoundError — if the entity does not exist. Use .get("bedroom") for safe access that returns None when the entity is absent.
Direct Entity Access
Use self.states.get(entity_id) when you have a full entity ID and don't need to specify the domain or state class. It automatically resolves to the correct domain-specific type (e.g., LightState for light.*), or falls back to BaseState for unregistered domains.
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
For domains that don't have a dedicated helper, or for dynamic access, provide the state class to the self.states dictionary-like interface:
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)
Iteration
You can iterate over domains to find entities.
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)
DomainStates Collection Interface
Every domain accessor (e.g., self.states.light) returns a DomainStates object. Beyond iteration, it supports the following operations:
| Operation | Example | Notes |
|---|---|---|
| Bracket access | self.states.light["bedroom"] |
Raises KeyError if absent |
| Safe access | self.states.light.get("bedroom") |
Returns None if absent |
| Containment | "bedroom" in self.states.light |
|
| Length | len(self.states.light) |
Number of entities in domain |
| Iteration (items) | for entity_id, state in self.states.light |
Lazy; same as .items() |
.items() |
self.states.light.items() |
Iterator of (entity_id, state) pairs |
.keys() |
self.states.light.keys() |
Eager list of entity IDs |
.iterkeys() |
self.states.light.iterkeys() |
Lazy iterator of entity IDs |
.values() |
self.states.light.values() |
Eager list of states |
.itervalues() |
self.states.light.itervalues() |
Lazy iterator of states |
.to_dict() |
self.states.light.to_dict() |
Eager dict[entity_id, state] |
Prefer lazy iteration for large domains
.items(), .iterkeys(), and .itervalues() are lazy and avoid validating every entity up front. .keys(), .values(), and .to_dict() are eager — they walk the entire domain immediately.
Built-in State Types
Hassette ships typed state classes for every standard Home Assistant domain. Import them from hassette.models.states (or via the states alias imported from hassette):
from hassette import states # pyright: ignore[reportUnusedImport]
# e.g. states.LightState, states.SunState, states.BinarySensorState
Full list of built-in state classes
| Domain | Class |
|---|---|
ai_task |
AiTaskState |
air_quality |
AirQualityState |
alarm_control_panel |
AlarmControlPanelState |
assist_satellite |
AssistSatelliteState |
automation |
AutomationState |
binary_sensor |
BinarySensorState |
button |
ButtonState |
calendar |
CalendarState |
camera |
CameraState |
climate |
ClimateState |
conversation |
ConversationState |
counter |
CounterState |
cover |
CoverState |
date |
DateState |
datetime |
DateTimeState |
device_tracker |
DeviceTrackerState |
event |
EventState |
fan |
FanState |
humidifier |
HumidifierState |
image_processing |
ImageProcessingState |
input_boolean |
InputBooleanState |
input_button |
InputButtonState |
input_datetime |
InputDatetimeState |
input_number |
InputNumberState |
input_select |
InputSelectState |
input_text |
InputTextState |
light |
LightState |
lock |
LockState |
media_player |
MediaPlayerState |
notify |
NotifyState |
number |
NumberState |
person |
PersonState |
remote |
RemoteState |
scene |
SceneState |
script |
ScriptState |
select |
SelectState |
sensor |
SensorState |
siren |
SirenState |
stt |
SttState |
sun |
SunState |
switch |
SwitchState |
text |
TextState |
time |
TimeState |
timer |
TimerState |
todo |
TodoState |
tts |
TtsState |
update |
UpdateState |
vacuum |
VacuumState |
valve |
ValveState |
water_heater |
WaterHeaterState |
weather |
WeatherState |
zone |
ZoneState |
For domains not in this list (custom integrations, third-party add-ons), see Custom State Classes.
Good to Know
Startup and staleness
The cache is populated once at startup via a full API fetch, then kept current by WebSocket state_changed events — so states are available as soon as your app's on_ready hook runs. A periodic background poll (default every 30 seconds) guards against any events that were missed. During a HA reconnect the cache is temporarily cleared; the StateProxy marks itself not-ready and retries reads automatically, so your code does not need to handle this case.
Missing entities
self.states.light.get("bedroom") returns None when the entity is absent. self.states.light["bedroom"] raises KeyError. If you are not certain an entity exists, prefer .get() and check the result before use.
See Also
- API - Entities & States - Retrieve states via API
- Bus - Subscribe to state change events
- App Cache - Cache data locally across restarts
- Custom States - Define custom state models