Skip to main content
The state machine is the heart of Home Assistant. It tracks the current state of all entities and fires events when states change. Understanding state management is crucial for building reliable integrations.

The State Machine

The StateMachine class manages all entity states in Home Assistant:
from homeassistant.core import HomeAssistant

def example(hass: HomeAssistant):
    # Access the state machine
    state_machine = hass.states
    
    # Get current state
    state = state_machine.get("light.living_room")
    
    # Set state
    state_machine.async_set(
        "sensor.temperature",
        "23.5",
        {"unit_of_measurement": "°C"}
    )
Reference: homeassistant/core.py:2058

State Objects

State Structure

The State object contains complete entity information:
from homeassistant.core import State

state = hass.states.get("light.living_room")

# Core properties
entity_id: str = state.entity_id          # "light.living_room"
domain: str = state.domain                # "light"
object_id: str = state.object_id          # "living_room"
state_value: str = state.state            # "on", "off", etc.

# Metadata
attributes: dict = state.attributes       # Extra data
context: Context = state.context          # Who/what caused change

# Timestamps
last_changed: datetime = state.last_changed    # When state changed
last_updated: datetime = state.last_updated    # When state/attrs updated
last_reported: datetime = state.last_reported  # When last written
Reference: homeassistant/core.py:1718

State vs Attributes

  • State: The primary status (on/off, open/closed, numeric value)
  • Attributes: Additional context (brightness, color, temperature)
# Light state
state = "on"  # State is simple string
attributes = {
    "brightness": 255,
    "color_temp": 370,
    "friendly_name": "Living Room Light",
    "supported_features": 43
}

Reading State

Get Single State

from homeassistant.core import State

# Get state (returns None if not found)
state = hass.states.get("light.bedroom")

if state is None:
    _LOGGER.warning("Entity not found")
else:
    _LOGGER.info(f"Light is {state.state}")
    brightness = state.attributes.get("brightness")
Reference: homeassistant/core.py:2142

Check State Value

# Quick check if entity has specific state
if hass.states.is_state("light.bedroom", "on"):
    _LOGGER.info("Bedroom light is on")
Reference: homeassistant/core.py:2151

Get All States

# Get all states
all_states = hass.states.async_all()

# Filter by domain
all_lights = hass.states.async_all("light")
all_sensors = hass.states.async_all(["sensor", "binary_sensor"])

# Count entities
light_count = hass.states.async_entity_ids_count("light")
Reference: homeassistant/core.py:2124

Get Entity IDs

# Get all entity IDs
all_entity_ids = hass.states.async_entity_ids()

# Filter by domain
light_ids = hass.states.async_entity_ids("light")
climate_switch_ids = hass.states.async_entity_ids(["climate", "switch"])
Reference: homeassistant/core.py:2081

Setting State

Basic State Update

from homeassistant.core import HomeAssistant

@callback
def update_sensor(hass: HomeAssistant):
    """Update sensor state."""
    hass.states.async_set(
        "sensor.my_sensor",
        "42",
        {
            "unit_of_measurement": "units",
            "device_class": "temperature",
            "friendly_name": "My Sensor"
        }
    )
Reference: homeassistant/core.py:2247

State Update with Context

Context tracks what triggered the state change:
from homeassistant.core import Context

# Create context (e.g., for automation)
context = Context(user_id="automation.turn_on_lights")

hass.states.async_set(
    "light.living_room",
    "on",
    {"brightness": 255},
    context=context
)
Reference: homeassistant/core.py:1213

Force Update

Force a state change event even if state hasn’t changed:
# Force update - fires event even if state/attributes unchanged
hass.states.async_set(
    "sensor.refresh",
    state.state,
    state.attributes,
    force_update=True  # Always fire EVENT_STATE_CHANGED
)
Reference: homeassistant/core.py:2247

State Change Events

EVENT_STATE_CHANGED

Fired when state or attributes change:
from homeassistant.const import EVENT_STATE_CHANGED
from homeassistant.core import Event, EventStateChangedData

@callback
def handle_state_change(event: Event[EventStateChangedData]) -> None:
    """Handle state change."""
    entity_id = event.data["entity_id"]
    old_state = event.data["old_state"]  # None if new entity
    new_state = event.data["new_state"]  # None if removed
    
    if old_state is None:
        _LOGGER.info(f"New entity: {entity_id}")
    elif new_state is None:
        _LOGGER.info(f"Removed entity: {entity_id}")
    else:
        _LOGGER.info(
            f"{entity_id}: {old_state.state} -> {new_state.state}"
        )

hass.bus.async_listen(EVENT_STATE_CHANGED, handle_state_change)
Reference: homeassistant/core.py:137

EVENT_STATE_REPORTED

Fired when state is updated but unchanged:
from homeassistant.const import EVENT_STATE_REPORTED
from homeassistant.core import Event, EventStateReportedData

@callback
def handle_state_reported(event: Event[EventStateReportedData]) -> None:
    """Handle state report (no change)."""
    entity_id = event.data["entity_id"]
    new_state = event.data["new_state"]
    last_reported = event.data["last_reported"]
    old_last_reported = event.data["old_last_reported"]
    
    _LOGGER.debug(
        f"{entity_id} reported at {last_reported}, still {new_state.state}"
    )
Reference: homeassistant/core.py:147

State Reservations

Reserve Entity ID

Reserve an entity ID before creating the entity:
@callback
def reserve_entity(hass: HomeAssistant, entity_id: str):
    """Reserve entity ID to prevent conflicts."""
    # Reserve the ID
    hass.states.async_reserve(entity_id)
    
    # Now safe to create entity
    # ... entity setup code ...
    
    # Finally set initial state
    hass.states.async_set(entity_id, "unknown", {})
Reference: homeassistant/core.py:2222

Check Availability

if hass.states.async_available("light.new_light"):
    # Entity ID is available
    hass.states.async_reserve("light.new_light")
else:
    _LOGGER.error("Entity ID already in use")
Reference: homeassistant/core.py:2239

Removing State

Remove Entity State

# Remove entity from state machine
removed = hass.states.async_remove("sensor.old_sensor")

if removed:
    _LOGGER.info("Entity removed")
else:
    _LOGGER.warning("Entity not found")
This fires EVENT_STATE_CHANGED with new_state=None. Reference: homeassistant/core.py:2169

State Persistence

Internal State Format

States are internally compressed for efficiency:
# Compressed state format (used in websocket and recorder)
compressed = {
    "s": "on",              # state
    "a": {"brightness": 255},  # attributes
    "c": "context_id",      # context (or full dict)
    "lc": 1234567890.0,     # last_changed timestamp
    "lu": 1234567891.0,     # last_updated (optional if == lc)
}
Reference: homeassistant/core.py:1708

Advanced State Patterns

State Validation

from homeassistant.exceptions import InvalidStateError
from homeassistant.core import validate_state

try:
    # State must be <= 255 characters
    validated = validate_state(user_input)
    hass.states.async_set("sensor.test", validated, {})
except InvalidStateError as err:
    _LOGGER.error("Invalid state: %s", err)
Reference: homeassistant/core.py:200

Entity ID Validation

from homeassistant.core import valid_entity_id, split_entity_id

# Validate format
if not valid_entity_id("light.living_room"):
    raise ValueError("Invalid entity ID")

# Split into domain and object_id  
domain, object_id = split_entity_id("light.living_room")
# domain = "light", object_id = "living_room"
Reference: homeassistant/core.py:192, homeassistant/core.py:171

Tracking Multiple Entities

from homeassistant.helpers.event import async_track_state_change_event

@callback
def handle_changes(event: Event) -> None:
    """Handle state changes for tracked entities."""
    entity_id = event.data["entity_id"]
    new_state = event.data["new_state"]
    _LOGGER.info(f"{entity_id} changed to {new_state.state}")

# Track multiple entities efficiently
remove = async_track_state_change_event(
    hass,
    ["light.living_room", "light.bedroom", "light.kitchen"],
    handle_changes
)

# Clean up
remove()
Reference: homeassistant/helpers/event.py:309

State Attribute Updates

Update only attributes without changing state:
# Get current state
current = hass.states.get("climate.thermostat")

if current:
    # Update attributes while keeping same state
    new_attrs = dict(current.attributes)
    new_attrs["temperature"] = 22
    
    hass.states.async_set(
        current.entity_id,
        current.state,  # Same state
        new_attrs       # Updated attributes
    )
    # last_changed stays same, last_updated updates

State Machine Indexes

The state machine maintains indexes for performance:

Domain Index

# Internally indexed by domain for fast lookups
class States(UserDict):
    def __init__(self):
        self._domain_index: dict[str, dict[str, State]] = {}
    
    # Fast domain filtering
    def domain_states(self, domain: str):
        return self._domain_index.get(domain, {}).values()
Reference: homeassistant/core.py:2016

Best Practices

1. Use Async Methods

Always use async methods in the event loop:
# GOOD
@callback
def my_callback():
    state = hass.states.get("light.test")  # Synchronous read OK
    hass.states.async_set("light.test", "on", {})  # Async write

# BAD
def blocking_function():
    hass.states.set("light.test", "on", {})  # Blocks event loop

2. Check for None

Always handle missing states:
state = hass.states.get("sensor.might_not_exist")
if state is None:
    _LOGGER.warning("Sensor not found")
    return

value = float(state.state)

3. Use Appropriate Tracking

Use helpers instead of manual event listening:
# GOOD - Optimized tracking
from homeassistant.helpers.event import async_track_state_change_event

remove = async_track_state_change_event(
    hass, ["light.test"], callback
)

# BAD - Manual tracking is slower
remove = hass.bus.async_listen(
    EVENT_STATE_CHANGED,
    lambda event: callback(event) if event.data["entity_id"] == "light.test" else None
)

4. Minimize State Updates

Only update when necessary:
# Check before updating
current = hass.states.get("sensor.test")
if current is None or current.state != new_value:
    hass.states.async_set("sensor.test", new_value, attrs)

5. Use Context

Provide context for state changes:
context = Context(
    user_id=automation.entity_id,
    parent_id=trigger_context.id
)

hass.states.async_set(
    "light.automated",
    "on",
    {},
    context=context
)

Performance Considerations

  1. State reads are fast - Direct dictionary lookup
  2. State writes fire events - Consider batch updates
  3. Domain filtering is optimized - Uses internal index
  4. Attributes are immutable - Uses ReadOnlyDict for safety

Common Pitfalls

  • Don’t modify state.attributes directly (it’s read-only)
  • Don’t assume entities exist (always check for None)
  • Don’t create excessive state updates (batch when possible)
  • Don’t store large data in attributes (use data registry)
  • Clean up entity states when removing entities

State Machine Lifecycle

# 1. Reserve entity ID (optional, prevents races)
hass.states.async_reserve("sensor.new")

# 2. Create initial state
hass.states.async_set("sensor.new", "unknown", {})

# 3. Update state over time
hass.states.async_set("sensor.new", "23.5", {"unit": "°C"})

# 4. Remove when done
hass.states.async_remove("sensor.new")

Build docs developers (and LLMs) love