Mastering State Machines in Home Assistant: Building Robust, Multi-Stage Automations

NGC 224
DIY Smart Home Creator
Standard Home Assistant automations, while powerful, can become brittle and complex when trying to manage multi-stage processes. Imagine automating a laundry cycle: detect machine on, wait for wash completion, then detect dry cycle start, and finally, notify when done. A series of if/then
conditions across multiple automations can quickly lead to race conditions, missed states, and debugging nightmares. This fragility often results in unreliable smart home experiences, frustrating tech enthusiasts and practical homeowners alike.
The elegant solution lies in implementing a state machine. By explicitly defining distinct states and the allowed transitions between them, you can create automations that are not only robust and predictable but also easier to understand, debug, and scale. This guide will walk you through building sophisticated state machines in Home Assistant, transforming your complex, multi-stage automations into rock-solid, reliable systems.
What is a State Machine?
At its core, a state machine is a mathematical model of computation. It's an abstract machine that can be in exactly one of a finite number of states at any given time. The machine changes from one state to another (a "transition") when initiated by an event or condition. In Home Assistant, we can simulate this by using an input_select
helper to represent the current state and a series of automations and scripts to manage state transitions based on sensor data and logical conditions. This approach enforces a clear, sequential flow, preventing illogical or conflicting actions.
Step-by-Step Setup: Building a Laundry State Machine
Let's build a practical example: a state machine for tracking your laundry. This machine will have four states: idle
, washing
, drying
, and finished
.
1. Define Your States with an input_select
Helper
First, create an input_select
helper to hold the current state of your laundry process. This is the central component of our state machine.
# configuration.yaml or helpers.yaml
input_select:
laundry_state:
name: Laundry Cycle State
options:
- idle
- washing
- drying
- finished
initial: idle
icon: mdi:washing-machine
After adding this, restart Home Assistant or reload Input Selects. You should see input_select.laundry_state
in your Developer Tools.
Screenshot Placeholder: Screenshot of input_select.laundry_state
in Developer Tools.
2. Create Template Sensors for Machine Status
We'll need a way to detect if your washing machine or dryer is running. This is often done by monitoring power consumption with a smart plug (e.g., Shelly Plug, Zooz Zen15) or vibration sensors. For this example, let's assume you have smart plugs reporting power. We'll create simple template binary sensors for "running" status.
# configuration.yaml or sensors.yaml
template:
- binary_sensor:
- name: Washing Machine Running
unique_id: washing_machine_running
state: "{{ states('sensor.washing_machine_power') | float(0) > 10 }}" # Adjust threshold based on your machine's idle vs. running power
delay_off:
seconds: 120 # Prevent false negatives during brief power dips
device_class: running
- name: Dryer Running
unique_id: dryer_running
state: "{{ states('sensor.dryer_power') | float(0) > 10 }}" # Adjust threshold
delay_off:
seconds: 120 # Prevent false negatives
device_class: running
Adjust sensor.washing_machine_power
and sensor.dryer_power
to your actual power entities and set appropriate power thresholds (e.g., 10W for idle, much higher for running). The delay_off
ensures stability.
3. Implement State Transition Automations
Now, we'll create automations that trigger state changes in input_select.laundry_state
based on our machine sensors.
Automation: Idle to Washing
This automation triggers when the washing machine starts and the laundry state is idle
.
# automations.yaml
- id: laundry_state_idle_to_washing
alias: "Laundry State: Idle to Washing"
trigger:
- platform: state
entity_id: binary_sensor.washing_machine_running
to: "on"
condition:
- condition: state
entity_id: input_select.laundry_state
state: "idle"
action:
- service: input_select.select_option
target:
entity_id: input_select.laundry_state
data:
option: "washing"
- service: persistent_notification.create # Optional: notify state change
data:
message: "Washing machine started. Laundry state is now 'washing'."
title: "Laundry Update"
mode: single
Automation: Washing to Drying
This triggers when the washing machine finishes (goes off) and the dryer starts, and the state is washing
. This assumes you move laundry immediately. A more robust system might have a "waiting for dryer" state.
# automations.yaml
- id: laundry_state_washing_to_drying
alias: "Laundry State: Washing to Drying"
trigger:
- platform: state
entity_id: binary_sensor.dryer_running
to: "on"
condition:
- condition: state
entity_id: input_select.laundry_state
state: "washing"
action:
- service: input_select.select_option
target:
entity_id: input_select.laundry_state
data:
option: "drying"
- service: persistent_notification.create
data:
message: "Dryer started. Laundry state is now 'drying'."
title: "Laundry Update"
mode: single
Automation: Drying to Finished
When the dryer finishes, and the state is drying
.
# automations.yaml
- id: laundry_state_drying_to_finished
alias: "Laundry State: Drying to Finished"
trigger:
- platform: state
entity_id: binary_sensor.dryer_running
to: "off"
for:
minutes: 5 # Ensure it's truly off
condition:
- condition: state
entity_id: input_select.laundry_state
state: "drying"
action:
- service: input_select.select_option
target:
entity_id: input_select.laundry_state
data:
option: "finished"
- service: persistent_notification.create
data:
message: "Dryer finished! Laundry state is now 'finished'. Time to fold!"
title: "Laundry Update"
- service: script.turn_on # Optional: Trigger a voice announcement
target:
entity_id: script.announce_laundry_finished
mode: single
Automation: Manual Reset (Optional)
You might want an easy way to reset the state back to idle
, perhaps after you've folded the laundry or if something goes wrong. An input_button
or a simple automation to reset after a delay works well.
# automations.yaml
- id: laundry_state_finished_to_idle
alias: "Laundry State: Finished to Idle (Manual/Delay)"
trigger:
- platform: state
entity_id: input_select.laundry_state
to: "finished"
for:
minutes: 60 # Automatically reset after 1 hour if not manually reset
action:
- service: input_select.select_option
target:
entity_id: input_select.laundry_state
data:
option: "idle"
- service: persistent_notification.create
data:
message: "Laundry cycle reset to 'idle'."
title: "Laundry Update"
mode: single
Alternatively, create an input_button
and an automation that triggers on its press to reset the state immediately.
# configuration.yaml
input_button:
reset_laundry_cycle:
name: Reset Laundry Cycle
icon: mdi:restart
# automations.yaml
- id: laundry_state_reset_button
alias: "Laundry State: Reset Button"
trigger:
- platform: state
entity_id: input_button.reset_laundry_cycle
to: "{{ states('input_button.reset_laundry_cycle') }}" # Trigger on any state change (button press)
action:
- service: input_select.select_option
target:
entity_id: input_select.laundry_state
data:
option: "idle"
- service: persistent_notification.create
data:
message: "Laundry cycle manually reset to 'idle'."
title: "Laundry Update"
mode: single
Troubleshooting Section
State Not Transitioning as Expected
- Check Sensor Data: Verify that
binary_sensor.washing_machine_running
andbinary_sensor.dryer_running
are accurately reflecting your machines' status. Use Developer Tools -> States to monitor their values in real-time. - Review Automation Traces: Home Assistant's automation tracing (Config -> Automations -> [Your Automation] -> Traces) is invaluable. Check if the trigger fired, if conditions were met, and if the action executed successfully. Look for "not met" conditions or "skipped" actions.
- Input Select State: Ensure
input_select.laundry_state
is in the expected state before a transition. For example, the "Idle to Washing" automation requires it to beidle
. - Service Call Errors: In traces, confirm that
input_select.select_option
service calls are succeeding without errors.
False Positives or Negatives from Power Sensors
- Adjust Thresholds: The power threshold (e.g.,
> 10
) is crucial. Use your smart plug's historical data to find a reliable value for when the machine is truly idle versus actively running. Some machines have "phantom load" even when off. - Refine
delay_off
: If sensors briefly report "off" during a cycle (e.g., between rinse and spin), increase thedelay_off
in your template binary sensors. This adds hysteresis, making the sensor more stable. - Consider Multiple Inputs: For maximum reliability, combine power sensing with other indicators like vibration sensors (e.g., Aqara vibration sensor) or even audio detection (e.g., with ESPHome and a microphone). Use a
template
binary sensor with multiple conditions for a more robust "running" state.
Race Conditions or Unexpected State Skips
mode: single
: Ensure your state transition automations usemode: single
(the default) orqueued
to prevent multiple instances from running concurrently, which can lead to unexpected behavior.- Clear Conditions: Each automation should have clear conditions ensuring it only acts when the state machine is in the preceding state. This prevents skipping states (e.g., directly from
idle
tofinished
).
Advanced Config / Optimization
Manual State Overrides and Error Handling
Sometimes you need to manually intervene or correct a state. An input_button
helper, as shown in the reset example, is perfect for manual resets. For more granular control, you could expose the input_select.laundry_state
in Lovelace with a dropdown.
# Example Lovelace card (Entities card)
type: entities
entities:
- entity: input_select.laundry_state
- entity: input_button.reset_laundry_cycle
title: Laundry Control
You can also build an "emergency reset" automation that triggers if a state remains unchanged for an unreasonably long time, indicating a stuck state.
Dynamic Notifications and Announcements
Leverage the state changes to trigger context-aware notifications. For instance, when input_select.laundry_state
changes to finished
, trigger a voice announcement on your smart speakers or send a rich notification to your phone.
# scripts.yaml
announce_laundry_finished:
alias: "Announce Laundry Finished"
sequence:
- service: tts.google_say # Or your preferred TTS service
data:
entity_id: media_player.living_room_speaker
message: "The laundry cycle is complete. Time to fold!"
- service: notify.mobile_app_your_phone # Or other notification service
data:
message: "Laundry Finished! Don't forget to fold."
title: "Home Assistant"
data:
priority: high
channel: laundry_alerts
Scaling State Machines for Multiple Devices/Processes
The beauty of this pattern is its reusability. You can apply the same state machine logic to other multi-stage processes:
- Dishwasher:
idle
->running
->finished
- Garage Door:
closed
->opening
->open
->closing
->closed
- Complex Security System:
disarmed
->arming_home
->armed_home
->arming_away
->armed_away
->alarm_triggered
For each, you would define a separate input_select
(e.g., input_select.dishwasher_state
) and corresponding sensors and automations. Using packages in Home Assistant (where related configurations for a feature are grouped in one file) can help manage this complexity.
Real-World Example: Advanced HVAC Control with Seasonal Modes
Consider an HVAC system that needs to operate differently based on the season, outside temperature, and occupancy. A state machine can manage its mode of operation.
States: off
, heating_winter
, cooling_summer
, fan_only_transitional
, away_eco
input_select
: input_select.hvac_operating_mode
Sensors: sensor.outside_temperature
, binary_sensor.home_occupancy
, input_boolean.winter_mode
, input_boolean.summer_mode
Example Transition (simplified):
# Automation: Transition to Heating in Winter
- id: hvac_transition_to_heating_winter
alias: "HVAC: Transition to Heating (Winter)"
trigger:
- platform: numeric_state
entity_id: sensor.outside_temperature
below: 10 # C
for:
hours: 2
condition:
- condition: state
entity_id: input_select.hvac_operating_mode
state: ["off", "fan_only_transitional", "away_eco"] # Only transition from these states
- condition: state
entity_id: input_boolean.winter_mode
state: "on"
- condition: state
entity_id: binary_sensor.home_occupancy
state: "on"
action:
- service: input_select.select_option
target:
entity_id: input_select.hvac_operating_mode
data:
option: "heating_winter"
- service: climate.set_hvac_mode # Adjust HVAC to heating mode
target:
entity_id: climate.main_thermostat
data:
hvac_mode: heat
mode: single
This state machine, combined with other automations that set thermostat target temperatures based on the hvac_operating_mode
, creates a highly intelligent and robust HVAC control system that accounts for multiple environmental and occupancy factors. Each mode can have its own set of rules, making the entire system much clearer than a single, monolithic automation.
Best Practices / Wrap-up
Clear Naming Conventions
Use clear, descriptive names for your input_select
entities, options, and automations. This greatly improves readability and maintainability, especially as your smart home grows.
Modular Design with Scripts
For more complex actions or common sequences of events during a state transition, encapsulate them in Home Assistant scripts. Instead of directly calling multiple services in an automation's action, call a single script. This makes your automations cleaner and promotes reusability.
# Example script to transition state and notify
script:
set_laundry_state:
fields:
new_state:
name: New State
required: true
selector:
text:
sequence:
- service: input_select.select_option
target:
entity_id: input_select.laundry_state
data:
option: "{{ new_state }}"
- service: persistent_notification.create
data:
message: "Laundry state changed to '{{ new_state }}'."
title: "Laundry Update"
Then, your automation's action would be:
action:
- service: script.set_laundry_state
data:
new_state: "washing"
Visualize States in Lovelace
Create Lovelace dashboards or specific cards (e.g., toggle-lock-card, Tile card, or a simple Entities card) to display the current state of your machines. This provides immediate feedback and allows for manual intervention when needed.
Thorough Testing and Debugging
State machines require rigorous testing. Use the Developer Tools to manually set input_select
states and trigger your sensor entities to simulate conditions. Leverage automation traces extensively to follow the logic flow. Consider using a separate "test mode" input_boolean
to prevent real-world disruptions during development.
Backup and Version Control
As you build complex logic, ensure your Home Assistant configuration is regularly backed up. Using Git for version control (as covered in other articles) is highly recommended for tracking changes and reverting if necessary.
By adopting the state machine pattern, you elevate your Home Assistant automations from reactive scripts to robust, predictable, and maintainable systems. This approach provides a solid foundation for handling even the most intricate multi-stage processes in your smart home, leading to a more reliable and enjoyable experience.

NGC 224
Author bio: DIY Smart Home Creator