The vacuum only runs when the chairs are up

A ball tilt switch in a cheap Zigbee door sensor tells Home Assistant the chairs are up, so the robot vacuum only runs when we're out and the floor is clear.

Difficulty
Easy
Parts cost
About A$3 of tilt switches, plus a Zigbee door sensor per chair (mine came in a pack for about A$39)
Build time
An evening
Skills
Soldering, Home Assistant

The problem

I wanted the robot vacuum to run by itself while we’re out. My partner’s rule was simple: it only runs by itself when the chairs are up, flipped onto the bench, so it isn’t working round chair legs. We only put the chairs up when we want a vacuum, so the chairs going up is the request. All I needed was a way for Home Assistant to know a chair was upside down.

How it works

How the chairs decide when the vacuum runs A ball tilt switch under the chair closes when the chair is flipped upside down. It is wired into a Zigbee door sensor, which reports "closed" to Home Assistant. Home Assistant checks that everyone's phones say they're away, the chairs have been up for two minutes, it's inside the time window, the vacuum is docked and it hasn't run today. Only then does the robot vacuum start. A sensor that has gone offline counts as chairs down. Tilt switch under the chair closed when the chair is upside down Zigbee door sensor reports "closed", as if a door shut Home Assistant ✓ everyone's phones say they're away ✓ chairs up for at least 2 minutes ✓ inside the time window ✓ vacuum docked, no error ✓ hasn't run yet today Robot vacuum cleans, docks, then reminds you A sensor that's gone offline counts as chairs down.

A ball tilt switch is a tiny metal can with a ball inside. Tip it one way and the ball rolls onto two contacts and closes the circuit. Tip it the other way and it opens. Inside a cheap Zigbee door sensor there’s a tiny three-legged chip, a hall-effect sensor, that pulls a signal line to ground when the door magnet comes close. I soldered the tilt switch from that signal line to ground, so the tilt switch does the magnet’s job. Now the sensor thinks “door closed” whenever the chair is upside down.

I set it closed when the chair is upside down and open when it’s upright. That way the switch sits open all day while people are sitting on the chair, and there’s nothing to chatter or flicker as someone shifts about.

Home Assistant then waits for two things together: everyone’s phones saying they’re away, and the chairs up for a couple of minutes. Then it starts the vacuum, once a day at most, inside a time window. When the vacuum docks again it sends a reminder to put the chairs back down, and when the kids get home with the chairs still up, their phones get told too.

I looked at Zigbee tilt and vibration sensors first. Most of the cheap ones only report “vibration”, not which way up they are. The few that do give angles cost more and are more complicated than this needs. A door sensor plus a few cents of tilt switch is simpler. It’s the same trick as my mailbox sensor: a cheap door sensor makes a handy battery Zigbee radio for anything that can close a contact.

What you need

  • Ball tilt switches, SW-520D or SW-200D type. They come in packs: https://www.aliexpress.com/item/4000540584730.html
  • A cheap Zigbee door/window sensor for each chair you want to watch: https://www.aliexpress.com/item/1005012129523396.html
  • Home Assistant with a working Zigbee setup. I use Zigbee2MQTT.
  • A robot vacuum that Home Assistant can start.
  • The Home Assistant phone app on everyone’s phone, so Home Assistant knows when you’re all out.
  • VHB tape (thick double-sided foam tape) to stick the sensor under the chair.
  • A soldering iron and a multimeter.

The plan is two sensors: one on a bar stool and one on a dining chair. With our rule (chairs only go up when we want a vacuum), two is plenty. You don’t need one on every chair. I’ve built one so far, and the config below shows two so you can see how to add more.

Wiring

There are only two connections:

Tilt switch Door sensor board
Leg 1 The hall sensor’s output (signal) leg
Leg 2 The hall sensor’s ground leg

The hall sensor is a three-legged surface-mount part: supply, ground and output. When a magnet is near, it pulls the output to ground, and the board reads that as “closed”. The tilt switch, wired from the output to ground, does exactly the same thing, and a ball tilt switch is just a contact, so it doesn’t matter which way round its legs go. My boards (marked ZB-SENSOR V2.0) have it marked Q1. Meter the legs on yours first: the output is the one that drops to 0 V when you hold a magnet near. Some sensors use a little glass reed switch instead. If yours does, solder across its two pads; it works the same way.

Fix the tilt switch so it can’t move against the sensor. If it can wobble, its idea of “upside down” drifts. Inside the sensor case if it fits, otherwise glued to the outside.

Firmware and config

There’s no firmware to write. The door sensor pairs like any other door sensor. Name it something like “Chair tilt” in Zigbee2MQTT and you’ll get binary_sensor.chair_tilt_contact.

A door sensor says off when it’s closed. Here closed means the chair is up, which reads backwards. So I made a template sensor that says on when the chairs are up. It also deals with the sensor going offline: a flat battery or a dropped Zigbee link shows as unavailable, which is not off, so the template reads not up. If Home Assistant can’t hear the sensor, the vacuum doesn’t run.

# configuration.yaml (or a template: package)
template:
  - binary_sensor:
      - name: "Chair up"
        unique_id: chair_up
        icon: mdi:chair-rolling
        # Each sensor reads "off" (closed) when its chair is upside down.
        # unavailable / unknown are not "off", so an offline sensor = chair NOT up.
        state: >
          {{ is_state('binary_sensor.chair_tilt_contact', 'off')
             and is_state('binary_sensor.stool_tilt_contact', 'off') }}

Only one chair? Delete the second line and the and.

Then the automation. zone.home holds a count of the people at home, so "0" means everyone’s phones say they’re out.

# automations.yaml
- id: vacuum_when_chairs_up
  alias: "Vacuum when everyone is out and the chairs are up"
  mode: single
  triggers:
    # Everyone has been away for 5 minutes (stops a phone's GPS blip starting it)
    - trigger: state
      entity_id: zone.home
      to: "0"
      for: "00:05:00"
    # Everyone left before the window opened
    - trigger: time
      at: "09:00:00"
  conditions:
    - condition: state
      entity_id: zone.home
      state: "0"
    - condition: state
      entity_id: binary_sensor.chair_up
      state: "on"
      for: "00:02:00"
    - condition: time
      after: "09:00:00"
      before: "15:00:00"
    # Docked means it's not already running and not stuck on an error
    - condition: state
      entity_id: vacuum.robot_vacuum
      state: docked
    # Not already run by this automation today
    - condition: template
      value_template: >
        {{ this.attributes.last_triggered is none
           or this.attributes.last_triggered < today_at() }}
  actions:
    - action: vacuum.start
      target:
        entity_id: vacuum.robot_vacuum

Pick a time window that suits your house. Keep both ends: a window with no end will happily start the vacuum at 11 at night.

If you’d rather name the people, swap the zone.home condition for this. It passes only when none of them is home:

    - condition: not
      conditions:
        - condition: state
          entity_id: person.example
          state: home
        - condition: state
          entity_id: person.example_2
          state: home

Either way, Home Assistant only knows about people whose phones run the app. A visitor sitting on the couch counts as nobody.

And the reminder. It watches the vacuum itself, not the automation, so it also works when someone starts a clean from the vacuum’s own app:

- id: chairs_down_reminder
  alias: "Remind to put the chairs down"
  mode: single
  triggers:
    - trigger: state
      entity_id: vacuum.robot_vacuum
      to: docked
      for: "00:02:00"
      not_from:
        - unavailable
        - unknown
  conditions:
    - condition: state
      entity_id: binary_sensor.chair_up
      state: "on"
  actions:
    - action: notify.mobile_app_example_phone
      data:
        title: "Vacuum's done"
        message: "The floor's clean. The chairs can come down."
        data:
          tag: chairs_down   # a second reminder replaces the first instead of stacking up

That reminder is the obvious one, and it’s the one that didn’t work. To an adult, chairs upside down on the bench mean “put me back”. To our kids, they’re just something to walk past. I’d get home to eight chairs with their legs in the air, and my partner, busy with the baby, wasn’t about to do all eight. So the reminder that actually matters goes to the kids. When one of them gets home and the chairs are still up, their own phone tells them:

- id: chairs_down_when_kids_get_home
  alias: "Chairs down: tell the kids when they get home"
  mode: single
  triggers:
    - trigger: state
      entity_id: person.example_kid
      to: home
  conditions:
    - condition: state
      entity_id: binary_sensor.chair_up
      state: "on"
  actions:
    - action: notify.mobile_app_example_kid
      data:
        title: "Chairs!"
        message: "The vacuum's been. Chairs down please, before Dad gets home."

Make a copy for each kid, with their own person and their own phone. That way it only goes to whoever just walked in, and nobody gets nagged about chairs while they’re still at school.

Step by step

  1. Meter-check a tilt switch. Set the meter to continuity, hold the switch one way then the other, and work out which way up it closes.
  2. Open the door sensor, find the hall sensor’s output and ground legs (or the reed switch’s two pads, if it has one) and solder the tilt switch across them.
  3. Hold the sensor the way it will sit under the chair. With the meter or the Zigbee2MQTT page open, check it reads open with the chair upright and closed with the chair upside down. Get this right now. It’s a pain once it’s taped on.
  4. Pair the sensor and name it (for example “Chair tilt”).
  5. Stick it under the seat with VHB tape, somewhere you can still get the battery out.
  6. Add the template sensor and check binary_sensor.chair_up follows the chair as you flip it.
  7. Add the two automations.
  8. Test it dry (below) before you let it start the vacuum for real.

Testing it

  • Never start the vacuum to test this. Test the logic, not the robot.
  • Flip the chair up and down and watch binary_sensor.chair_up in Developer tools → States.
  • Check the fail-safe: with the chair up, use Developer tools → States to set binary_sensor.chair_tilt_contact to unavailable. chair_up should drop to off. (This only changes what Home Assistant shows; the sensor’s next report puts it back.)
  • For the automation, swap vacuum.start for a phone notification that says “would have started the vacuum”. Leave it like that for a few days. Every day the chairs go up and everyone goes out, you should get exactly one message, inside the window.
  • Open the automation’s Traces to see which condition stopped it on the days it didn’t fire.
  • When the dry run looks right, put vacuum.start back and be home to watch the first real run.

Lessons learnt

“Every chair is up” is true when there are no chairs. My first plan checked that every chair sensor read “up”. With no sensors paired yet, that check has nothing to look at, and “all of nothing” counts as true. The vacuum would have run straight into the chair legs. So name each sensor in the template, treat offline as “not up”, and don’t switch the automation on until every sensor is paired and reading properly.

The reminder didn’t come when the vacuum was started from its app. My first reminder only fired after runs that Home Assistant had started. One day the clean was started from the vacuum’s own app with the chair up, and nobody was told to put it down. The reminder now watches the vacuum, not the automation, so any clean with the chairs up gets one.

A mid-job stop can send the reminder twice. A mopping vacuum goes back to wash its mop partway through a job. If that break runs past two minutes, the reminder goes out early and again at the end. The tag on the notification makes the second one replace the first instead of stacking up.