Laundry Monitoring Automation, Part 3: Final Config and Installation

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.

Full ESPHome Configuration

# 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

[collapse]

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-in list.
  • Detect when the machine finishes and send the final notification only to those on the Opted-in list, 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.

Washer Started Notification

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

[collapse]

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.

Handle Washer Notification Responses

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

[collapse]

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.

Washer Done Notification

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

[collapse]

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.

Laundry Monitoring Automation, Part 2: Soldering and 3D Printing

In Part 1, I built a proof-of-concept using an ESP8266 and vibration sensors to detect washer and dryer activity. The breadboard prototype had proved the idea worked, but it looked more like a science fair project than a laundry automation device. It was time to make it permanent. That meant dragging out the soldering iron, wrestling with too-thick wires, and learning to use heat shrink tubing along the way.

Of course, once the wiring was done, I needed a proper home for the controller and sensors. My 3D printer got a workout as I iterated through case designs — some good, some cracked, and some that never quite fit.

This part of the project was all about turning a fragile prototype into something that could survive real use, and as usual, the process wasn’t as straightforward as I first imagined.

Soldering

The Controller

With the breadboard test behind me, it was time to make things permanent. The ESP8266 D1 Mini only has one 5V pin and one ground pin, but I needed power for both sensors. My first thought was to try cramming two wires into a single hole — turns out, they were too thick for that.

So I improvised: I soldered one wire in normally, then cut the excess and attached another wire from the backside. Not the prettiest solution, but it worked.

Once everything was connected, I looked over the joints. Most were fine, but the extra wires I tacked onto the back left exposed sections that I wasn’t happy with. At first, I figured I’d just leave them — but the more I looked, the more it bothered me. This was the perfect excuse to try out some heat shrink tubing.

A quick Amazon order later, I slipped the tubing on, heated it up with the side of my soldering iron, and the results looked so much cleaner. Definitely worth the extra step.

It wasn’t flawless, but at least now the controller wiring looked sturdy enough to move on to the next stage.

The Sensors

Next up were the SW-420 vibration sensors. These modules came with header pins pre-soldered, which was convenient for breadboarding but not ideal for my ultimate use case. I considered removing the headers, but they were stubbornly attached, and I didn’t want to risk damaging the board.

So instead, I decided to solder the wires directly onto the existing headers. This worked, but it was fiddly; the joints ended up with more exposed metal than I liked, and the connections didn’t look as neat as I wanted. Once again, heat shrink tubing came to the rescue. Sliding it over the messy joints and shrinking it down made everything look far cleaner — and much sturdier.

The sensors were now wired up solidly and ready to be paired with the controller.

3D Printing

The ESP Case

With the wiring done, I needed a proper case for the controller. I dug up an old model I had lying around and modified it with a few cutouts for the wires. Version 1 came off the printer looking decent… until I tried fitting the ESP inside.

The holes were too small, the support platform blocked the bottom wires, and the cover clips pressed right on top of my solder joints. In short: it didn’t fit.

So I started over. For Version 2, I rebuilt the case from scratch. I fixed the tilt in the model, added full-height holes for the wires, moved the clips away from the solder joints, and even added some lettering to the cover.

The new case worked better and I was able to fit the D1 mini and cables, but the cover gave me headaches. I printed it with supports, which fused too tightly, and I ended up cracking it while trying to pry it apart. Not ideal. The ESP also did not sit as deep and snugly as I had hoped due to the wire underneath not allowing it to go all the way down but it was good enough.

I wasn’t ready to give up on the lettering, though. For Version 3, I changed the design so the text was cut into the cover instead of raised, and I printed it without support. This time, it came off cleanly without cracking.

The lettering was subtle, but it worked, and the case finally felt solid enough to house the controller.

The Sensor Case

After finishing the controller case, I turned my attention to the vibration sensors. I found a case model on Thingiverse that looked promising, with cutouts for the adjustment screw and indicator LEDs.

I printed it out… and the sensor didn’t fit. Too tight, wrong alignment, and no room to slide it in cleanly. So, back to the design software. I stretched the model, shifted the pillar that holds the sensor, and shortened the height a bit so it would sit snug. The first reprint was better, but still not quite right. It took one more iteration to finally get the case fitting the SW-420 modules properly.

The result was a simple but sturdy enclosure that kept the sensors secure while still exposing the adjustment screw. It wasn’t fancy, but it worked — and the sensors finally had a proper home.

Finishing Touches

With the controller and sensors each in their own cases, the last step was cleaning up the wiring. At this point, I had a jumble of wires sticking out in all directions, and I didn’t want to leave them loose.

So, I twisted the wires together into neat bundles, then crimped on JST connectors. That way, the sensors could be easily plugged in or disconnected without having to resolder anything.

It was a small detail, but it made the whole project feel much more polished. The wiring was tidy, modular, and finally looked like something I could install next to the washer and dryer.

Ready to Install

After plenty of soldering, shrinking, printing, and re-printing, the hardware was finally taking shape. The ESP had a case that fit, the sensors had snug enclosures, and the wiring was cleaned up with connectors.

What started as a breadboard full of loose jumpers now looked like a real device. It was finally ready to move out of the workshop and into the laundry room.

In the next part, I’ll tackle the final configuration, mounting the sensors to the machines, and getting the system up and running in Home Assistant.

Laundry Monitoring Automation, Part 1: From Idea to First Signals

The Problem

For a while, I’ve wished there was a way to get reliable notifications when my washer and dryer finished. Both machines live in the basement, and the end-of-cycle beeps aren’t loud enough to reach upstairs.

The obvious solution was to plug them into a smart outlet and track power usage. But my dryer runs on a 240-volt outlet, and I couldn’t find a reasonably priced smart plug that could handle it.

That meant I’d need some sort of sensor. The two common options I kept coming across were:

  • a sound sensor to listen for the end-of-cycle chime
  • a vibration sensor that would detect the machine running and then stopping.

I didn’t like the idea of listening for sounds — it felt finicky and prone to mistakes — so vibration sensors it was. The hardware side seemed simple enough, but the thought of writing custom software to make sense of the readings had me procrastinating for months.

Choosing the Approach

The turning point came when I revisited ESPHome. I’d tried it a few years ago for a different project, but at the time the flashing process was confusing and I couldn’t quite get it working. I eventually gave up and went with Tasmota, which was easier back then.

Fast forward to today: ESPHome has grown up a lot. It integrates directly with Home Assistant, flashing devices is much smoother, and I’d recently had a good experience setting up an ESPHome Bluetooth proxy. That gave me the confidence to give it another shot.

Around that same time, I found a project on GitHub called LaundryBot, which was almost exactly what I’d been planning. The author used an ESP32, but since I had a pile of ESP8266 boards on hand (and I didn’t need Bluetooth anyway), I figured I’d adapt it.

So, the plan became: pair of SW-420 vibration sensors feeding into an ESP8266 running ESPHome, reporting washer and dryer status updates straight into Home Assistant. With the concept settled, it was time to see if I could actually get it working.

Materials

For this project, I mostly used parts I already had lying around, with a few extras picked up along the way:

  • ESP8266 (Wemos D1 Mini clone) – pack of 10 from Amazon
  • SW-420 vibration sensor modules – pack of 5 from Amazon
  • 22 AWG solid core hookup wire – for sensor connections (from Amazon)
  • Breadboard & jumper wires – for prototyping
  • Soldering iron – to make permanent connections
  • 3D printer (Creality CR-6 SE) – for controller and sensor cases

Optional but nice to have:

  • Heat shrink tubing – to clean up exposed joints (from Amazon)
  • JST connectors & crimping tool – for detachable, neat wiring (from Amazon)

Initial Setup

With the plan in place, I grabbed a Wemos D1 Mini (an ESP8266 clone) from my parts bin and started setting up ESPHome. The GitHub repo I found suggested wiring up the sensor to an LED on the board for testing, but since my board didn’t have that, I decided to skip ahead and just try flashing ESPHome directly.

At first, everything looked promising: I plugged in the D1 Mini, Windows recognized it, and the drivers installed without complaint. But when I tried flashing through the ESPHome Device Builder in Home Assistant, it refused to connect.

I thought maybe the board wasn’t in flash mode, so I tried all sorts of combinations: grounding D3, grounding D8, adding resistors, swapping cables, different USB ports, even trying different boards. Nothing. After a few hours of chasing advice from forums and blog posts, I was ready to call the board defective.

In a last-ditch attempt, I dusted off my old Windows 10 laptop, plugged the board in, and opened the ESP Home Web Flasher (link). To my complete surprise, it connected instantly and flashed on the first try. Just like that, the board came alive, connected to WiFi, and popped up in Home Assistant.

That was the breakthrough I needed to put the project back on track — and finally get me excited to see some real signals.

Proof of Concept

Setting Up the Software

With the firmware running, I wanted to see if the ESP could actually talk to Home Assistant. To start small, I added a few simple ESPHome sensors: device status, WiFi signal, and uptime. That way I’d know right away if the board was online and working.

binary_sensor:
  - platform: status
    name: "LaundryBot"
sensor:
  - platform: wifi_signal
    name: "WiFi Signal"
  - platform: uptime
    type: seconds
    name: "Uptime Sensor"

The first time those values popped up in Home Assistant, it felt like a small victory — proof that the setup was alive.

Next came the vibration sensors. I Googled which pins were safe on the D1 Mini (link) and picked GPIO4 (D2) and GPIO14 (D5). I added them as binary sensors for “Washer” and “Dryer,” then flashed the config again.

binary_sensor:
  - platform: status
    name: "LaundryBot"
  - platform: gpio
    pin: GPIO4 #D2
    name: "washer"
    device_class: vibration
    filters:
     - delayed_on: 10ms
     - delayed_off: 1sec
  - platform: gpio
    pin: GPIO14 #D5
    name: "dryer"
    device_class: vibration
    filters:
     - delayed_on: 10ms
     - delayed_off: 1sec

Seeing those two new entities appear in Home Assistant — even if they were still “unavailable” with nothing connected — felt like another big step forward.

Trying out the Hardware

With the software set, it was time for some hands-on testing. I set everything up on a breadboard: ESP resting on loose headers, sensors connected with jumper wires.

When I gave the sensors a shake… nothing. I thought I’d wired them wrong. After a little research, I learned the SW-420 is only sensitive along one axis. In other words, I’d been shaking them the wrong way.

Once I slid the breadboard back and forth along the sensor’s axis, the logs lit up with vibration events.

Not everything was perfect — sometimes a sensor would “stick” in the high state and needed another shake to reset — but seeing those signals show up in Home Assistant was the real proof I needed. The idea was solid, and it was time to start building it for real.

Ready to Build

After a few dead ends and plenty of trial and error, I had a working proof of concept: an ESP8266 on a breadboard, two vibration sensors, and signals flowing into Home Assistant. The setup was imperfect but it was enough to confirm the approach would work.

In the next part, I’ll take this breadboard prototype and turn it into proper hardware: soldered connections, 3D-printed cases for the controller and sensors, and wiring that can stand up to real use in the laundry room.