In Part 2, I took the breadboard proof-of-concept from Part 1 and turned it into real hardware — soldered connections, 3D-printed cases, and wiring neat enough to survive in the laundry room.
With the hardware ready, it was finally time to move out of the workshop and into the basement. This part of the project was about making it real: mounting the controller and sensors on the machines, dialing in the vibration sensitivity, and wiring up the logic inside Home Assistant so it actually did something useful.
Updating the ESPHome Configuration
Before mounting everything, I wanted to make sure the ESPHome firmware was current. When I opened my config, I was greeted with a big yellow deprecation warning: my old API key format was no longer supported.
I also took the opportunity to add a couple of extra “maintenance” sensors — WiFi signal and uptime — so I could keep an eye on the device’s health once it was mounted out of reach.
After generating a new encryption key to replace the API key and updating the YAML, I flashed the board and it reconnected to Home Assistant without a hitch.
# Config for the ESP Device
esphome:
name: esp-laundry-bot
friendly_name: ESP Laundry Bot
min_version: 2025.5.0
name_add_mac_suffix: false
# Specifies the board being used
esp8266:
board: esp01_1m
# Enable logging on the serial port
# This is useful for seeing logs in HA and the Device Builder
logger:
# Enable the ESP API
# HA and the Device Builder use this to read the device
api:
encryption:
key: !secret api_key
# Allow Over-The-Air updates from the Device Builder
# Also set a password for OTA for security
ota:
- platform: esphome
password: !secret ota_password
# Wifi config to connect to my network
# Sets a static IP to avoid needing to obtain an IP
# Also sets a password for the backup Access Point
wifi:
ssid: !secret wifi_ssid
password: !secret wifi_password
manual_ip:
static_ip: 192.168.1.251
gateway: 192.168.1.1
subnet: 255.255.255.0
ap:
password: !secret ap_password
binary_sensor:
# Status of the device
- platform: status
name: "LaundryBot"
# Config for attached SW-420 sensor
- platform: gpio
pin: GPIO4 #D2
name: "washer"
device_class: vibration
filters:
- delayed_on: 10ms
- delayed_off: 1sec
# Config for other attached SW-420 sensor
- platform: gpio
pin: GPIO14 #D5
name: "dryer"
device_class: vibration
filters:
- delayed_on: 10ms
- delayed_off: 1sec
sensor:
# Sensor for the strength of the WiFi signal
- platform: wifi_signal
name: "WiFi Signal Sensor"
# Sensor providing the device uptime in seconds
- platform: uptime
type: seconds
name: Uptime Sensor
text_sensor:
# Sensor providing the WiFi SSID and the device's Mac Address
- platform: wifi_info
ssid:
name: ESP Connected SSID
mac_address:
name: ESP Mac Wifi Address
# Sensor providing the current ESPHome Version on the device
- platform: version
name: "ESPHome Version"
hide_timestamp: true
Mounting the Controller and Sensors
With the firmware updated, it was finally time to leave the workbench. I mounted the ESP case on the wall near the washer and dryer, close enough to power but out of the way and the sensors on the back of the machines. I attached them using double sided gorilla tape.
Once everything was mounted, I routed the cables neatly back to the controller and connected them using the JST plugs I’d added earlier.


With the hardware in place, I tweaked the tiny potentiometers on the SW-420 boards to dial in their sensitivity. A little turn too far caused constant false positives; too far the other way and they’d miss vibrations entirely. After a few small adjustments while watching Home Assistant logs, both sensors settled into a sweet spot.
Seeing the cases mounted neatly and the sensors responding reliably made the project finally feel like a real, working system.
Creating Home Assistant Automations
In our house, everyone does their own laundry, so a simple “send a notification to all phones” wasn’t going to cut it. I wanted a way for each person to decide whether they wanted to be notified for a particular load.
After some experimenting, I landed on a system that was a bit more involved but felt natural: as soon as the machine starts running, Home Assistant sends an actionable notification to everyone. That notification gives each person the option to opt in or opt out for that cycle.
Behind the scenes, a helper list keeps track of who tapped “yes.” Each tap is saved via an MQTT event, and when the machine finishes, the automation checks that list, sends notifications only to those who opted in, and then clears the list for next time.
For simplicity, I duplicated this setup for both the washer and the dryer rather than trying to build a single generic version.
Implementation
To make the opt-in system work, I started by creating two input_text helpers in Home Assistant. The first, Laundry Notification Users, holds a list of everyone who could be notified. The second, Opted-in Washer Notification Users, stores just the people who actually opted in for the current cycle.
With the helpers in place, I broke the logic into three separate automations:
- Detect when the machine starts and send the initial actionable notification to everyone
- Handle the responses to that notification, adding anyone who tapped “yes” to the
Opted-inlist. - Detect when the machine finishes and send the final notification only to those on the
Opted-inlist, then clear the list for the next cycle.
Detect machine running & send notification
When the washer (or dryer) has been vibrating for 5 minutes, Home Assistant sends an actionable notification to everyone in the Laundry Notification Users list. That notification includes “Opt In” and “Opt Out” buttons so each person can choose for that cycle. The 5 minute delay is to help eliminate any false positives.
alias: Washer Started Notification
description: ""
triggers:
- trigger: state
entity_id:
- binary_sensor.esp_laundry_bot_washer
from: "off"
to: "on"
for:
hours: 0
minutes: 5
seconds: 0
conditions: []
actions:
- repeat:
for_each: "{{ users }}"
sequence:
- data:
message: Washer is running. Notify you when it's done?
data:
actions:
- action: washer_notification_yes_{{ repeat.item }}
title: "Yes"
- action: washer_notification_no_{{ repeat.item }}
title: "No"
action: notify.notify_{{ repeat.item }}
variables:
users: >
{{ states('input_text.laundry_notification_users').split(',') | map('trim')
| reject('equalto', '') | list }}
mode: single
Handle notification responses
Next came the response handler. When someone taps one of the buttons on the actionable notification, Home Assistant fires a mobile_app_notification_action event. My automation listens for those events.
When processing the event, I parse the action ID to extract the user’s name (which I built into the action when sending the notification). If the action starts with “yes,” I add that user to the opt-in helper list. If the action starts with “no,” I do the opposite — check the helper list and remove the user if they’re on it.
alias: Handle Washer Notification Responses
description: ""
triggers:
- event_type: mobile_app_notification_action
trigger: event
conditions:
- condition: template
value_template: |
{{ trigger.event.data.action.startswith('washer_notification_yes_') or
trigger.event.data.action.startswith('washer_notification_no_') }}
actions:
- variables:
user: "{{ trigger.event.data.action.split('_')[-1] }}"
- choose:
- conditions:
- condition: template
value_template: >-
{{
trigger.event.data.action.startswith('washer_notification_yes_')
}}
sequence:
- data:
entity_id: input_text.opted_in_washer_notification_users
value: >
{% set users =
states('input_text.opted_in_washer_notification_users').split(',')
| map('trim') | list %} {% if user not in users %}
{{ (users + [user]) | join(',') }}
{% else %}
{{ users | join(',') }}
{% endif %}
action: input_text.set_value
- conditions:
- condition: template
value_template: >-
{{ trigger.event.data.action.startswith('washer_notification_no_')
}}
sequence:
- data:
entity_id: input_text.opted_in_washer_notification_users
value: >
{% set users =
states('input_text.opted_in_washer_notification_users').split(',')
| map('trim') | list %} {{ users | reject('equalto', user) |
join(',') }}
action: input_text.set_value
mode: single
Detect machine done & send final notification
Finally, I needed an automation to close the loop. When the washer (or dryer) stops running, Home Assistant already flips the “running” boolean off. My automation watches for that state change, but to avoid false triggers from short pauses I make it wait two minutes before firing.
When it does run, it pulls the Opted-in list from the helper and loops through each person, sending a “Finished” notification only to those who opted in. Once the messages go out, it clears the helper list so the next load starts fresh.
alias: Washer Done Notification
description: ""
triggers:
- entity_id: binary_sensor.esp_laundry_bot_washer
from: "on"
to: "off"
for:
minutes: 2
trigger: state
conditions: []
actions:
- repeat:
for_each: "{{ users }}"
sequence:
- data:
message: Washer is done!
action: notify.notify_{{ repeat.item }}
- data:
entity_id: input_text.opted_in_washer_notification_users
value: ""
action: input_text.set_value
variables:
users: >
{{ states('input_text.opted_in_washer_notification_users').split(',') |
map('trim') | reject('equalto', '') | list }}
mode: single
Wrap Up
What started as a quiet frustration — missed laundry beeps in the basement — turned into a build that taught me a ton. Along the way I learned how finicky vibration sensors can be, how ESPHome has matured into a powerful tool, and how breaking big problems into smaller automations saves endless headaches.
This final phase — installing, tuning, and wiring up the automations — was the most satisfying. Seeing the notifications pop up exactly when the machines finished felt like magic, even though I knew all the YAML and solder joints behind it.
There are still things I’d like to refine. I could make the opt-in logic more generic so I don’t have two separate sets of helpers and automations for washer and dryer. I’ll also have to see how reliable the vibration sensors prove to be. But for now, it works: a tidy little system that reliably tells us when our laundry is done and only pings the people who care.
If you missed the earlier parts, you can find them here: Part 1: From Idea to First Signals and Part 2: Building.


























