Oh boy, do I love stupid projects. And oh boy, do I love IoT. But most of all, I love solving minuscule problems in absolutely ridiculous ways.
So this is the story of how I used an ESP, magnets, and Kubernetes to solve the great toilet crisis of 2021.
I would have written this article far sooner, as it was my first contact with the IoT stack that I kept using for other (and more useful) home automation projects. However, some colleagues kept talking about “privacy concerns” and “professional boundaries.”
But now that enough time has passed, I hope I can finally freely share this tale of absolutely necessary workplace optimization.
Stories from the Open Source Smart Home (Office) - Part 4
The Great Toilet Crisis of 2021#
Picture this: a thriving tech company, twenty-something male engineers… and one toilet stall.
The daily dance was predictable: You’d feel nature’s call and walk over to the bathroom, only to find it occupied. Then you’d do the walk of shame back to your desk and sit there wondering whether it was free yet, whether you should check again, and how long was too long to wait.
One stall was already bad enough - but it was the he uncertainty turned every bathroom break into a gamble. You’d walk all the way to the bathroom (a whole 20 meters!), find it occupied, and then have to walk ALL THE WAY BACK to your desk. That’s 40 meters of completely unnecessary locomotion. Multiply that by 20 engineers, several times a day, and we’re talking about kilometers of wasted movement.
We were engineers. We over-engineered every microservice, every config, every deployment, and every CI/CD pipeline. But we couldn’t figure out if the toilet was free without physically walking there.
Obviously, this was unacceptable.

Standing on the Shoulders of Giants (Who Got Told Off)#
I wasn’t the first to recognize this engineering emergency. Some brave colleagues had already installed a prototype back in the old office - a suspiciously large box mounted on the toilet stall that screamed “SURVEILLANCE DEVICE” to anyone within a 10-meter radius.
It worked perfectly - for exactly three days, before the CEO asked them to take it down. Something about “professionalism” and “what will investors think.”
Sad engineering noises.
But as the team grew and the toilet traffic increased, the problem became impossible to ignore.
And I had spare evenings.
The Failed Experiments Hall of Fame#
Attempt #1: The Movement Sensor Hijack#
The toilet had an existing PIR sensor that controlled the lights. “Perfect,” I thought, “I’ll just tap into that.”

I ordered the exact same model from Amazon, cracked it open, and found just enough space for a TinyPICO, one of the smallest ESP32 boards available at the time. The plan was elegant: intercept the sensor signal, broadcast it, nobody would know.

Then I noticed the sensor ran on mains voltage. 230V AC. In a box of about 5 cm size. A sane person would have thought: “Hmm, shoving additional electronics into a mains-powered device that sits in a bathroom is probably a terrible idea. The risk of causing a short circuit that burns down the office or even kills someone is too high.”
But I’m not a sane person. I’m an engineer with a mission.

So naturally, I opened the sensor on my workbench and connected it to mains so I could map the voltages. With my multimeter ready, I carefully traced the circuit. I was being so professional about it - right up until the moment I accidentally touched the wrong part with my screwdriver.
230V AC straight into my hand.
The muscles in my arm did that fun involuntary contraction thing. The screwdriver went flying. The GFCI triggered. Several German words were invented.
Ouch.
Ok ok… Lesson learned… Moving on.
Attempt #2: The Obvious Box 2.0#
I considered recreating my colleagues’ box design but making it smaller and less conspicuous.
After trying every possible mounting position, however, I concluded that any external box would still look like we were filming a very specific genre of content.
Back to the drawing board.
Attempt #3: Door Surgery#
Maybe I could hide something in the door frame itself?
I spent a whole evening on the bathroom floor, removing the entire lock mechanism from the door and taking it apart completely, with every spring and pin laid out on paper towels around me.
The good news: I found space inside the door frame cavity.
The bad news: There was no way to get power there without visible cables.
The worse news: I had to reassemble a door lock with mysterious leftover screws.


The Breakthrough: Magnetic Fields#
Remember playing with magnets and compasses as a kid? Put a magnet near a compass and the needle moves. Basic physics that every 8-year-old knows.
Slide a piece of iron between them and it moves as well. The magnetic field lines are “pulled into” the magnetic conductor, which changes the field the compass sees. Nothing fancy.
What blew my mind was that the compass also reacted when I moved the iron behind the magnet, on the opposite side and quite a bit away from the compass. The iron changed the field around the magnet enough to affect the measurement in front of it.
So, while playing with magnetic sensors for an unrelated project (I swear), I had an epiphany. The door lock was metal. And magnetic fields… they go through walls.
I could detect the door state from inside the wall.

The Proof of Concept#
I needed to test this theory.
The wall was made of two layers of drywall, and I could access the cavity through the hole for the movement sensor (that I’d previously electrocuted myself with). I grabbed a Wemos D1 mini I had lying around, connected it to a magnetometer, and attached the whole mess to the end of a ridiculously long USB cable.
While I don’t have a picture of the actual sensor, here is a later revision of what I call a “Sensor on a Stick”:

Then came the funny part: feeding this contraption through the wall cavity. I’d deliberately stayed late at the office for this experiment, figuring I’d have privacy for my bathroom engineering session.
Of course, that’s exactly when a colleague returned to grab his forgotten laptop and found me sitting on the bathroom floor with a USB cable disappearing into the wall.
Should I… ask?
Probably not.
Cool. See you tomorrow.
He left. We never spoke of it again.
Anyways, the sensor ended up roughly where I estimated the strike plate of the door lock would be.
Compass in place.
Next, I placed a neodymium magnet in the strike plate hole. Its field immediately showed up in the sensor readings.
Magnet in place.
Then I locked the door in front of me, and…
It worked. Not just “kind of worked” - it worked perfectly.
With the magnet positioned just right, the steel lock produced different magnetometer readings as the door moved between open, closed, and locked. Sliding the bolt into place changed the XYZ values enough to give me three distinct states, all readable from inside the wall.
It was beautiful. It was elegant. It was completely invisible.
It needed a name.
The Heislberg Uncertainty Principle#
If you were wondering about the title of the post…
Werner Heisenberg was a German physicist. That means, he definitely had something to do with magnets. Probably.
I mean, he defenitely did something with uncertanty (of toilet occupancy I guess).
Plus, “Heisl” is Austro-Bavarian slang for a small house… or a toilet.
The joke practically wrote itself.
Anyways, now that the most difficult part of every tech project is done - naming it - I could finally focus on the permanent installation.
The Hardware#
Around this time, my colleague was building his house and kept talking about his DIY home automation setup. He’d discovered these OLIMEX ESP32-POE boards which he used all over the system, and he couldn’t stop praising them.
I asked if he could bring me a spare to try out. He did, I plugged it in, and immediately fell in love. The ESP ecosystem has amazing software support in general, the “32” in it meant enough processing power for anything I could throw at it, and Power over Ethernet meant no suspicious power cables running through the wall.
Basically: Perfect for toilet monitoring. (Also perfect for my sofa project, but that’s yet another story.)
For the magnetic sensor, I used a GY-271 breakout with ESPHome’s HMC5883L driver. It was easy to source, but I2C limited the cable length. In my setup, anything beyond 1-2 meters became unreliable.
The Installation#
The movement sensor I’d unsuccessfully tried to hack earlier? It has to get power from somewhere. The next evening, armed with my cable pull rod, I went spelunking in the office infrastructure.
Following the cable with my pull rod through the wall cavity and cable conduit led me up into the dropped ceiling. Popping my head up there with a ladder revealed several incredible discoveries:
- My cable pull rod’s other end
- An alarming amount of cobwebs
- A patch panel that went directly to our server room (jackpot!)

I’m not saying the building was designed for toilet monitoring, but I’m not not saying it either.
I pulled a four-wire telephone cable with pre-soldered terminal connectors (VCC, GND, SDA, SCL) through the existing conduit. Then I shoved the magnetometer into the wall cavity, positioning it right behind where I’d placed a small neodymium magnet in the strike plate hole. Finally, I put the ESP32-POE on a breadboard mounted to the ceiling using the finest installation method known to engineering: double-sided tape.

After connecting the I2C cable from the wall sensor to the ceiling-mounted ESP, I ran an Ethernet cable over to the patch panel. From there, it was a simple hop to the server room where I connected everything to a PoE injector on the guest network (the Network Admin had already given his blessing - he was surprisingly enthusiastic about the whole thing).
The Toilet (Data) Pipeline#
The “Edge Device”#
The ESP32-POE went through two iterations of firmware. Initially, I wrote it in C++ using libraries for the HMC5883L and MQTT. A simple polling loop read the sensor values and pushed them to the broker. It worked well for the first few weeks, but sometimes the sensor mysteriously got “stuck” without explanation. With no logs to debug with (because who needs logging?), we suspected a memory leak in one of the libraries. The only fix was a restart, which meant pulling out cables in the server room.
After using ESPHome in other projects, I migrated Heislberg as well. The YAML configuration was less likely to segfault the toilet, and proper logging gave us a chance of diagnosing the next failure. OTA updates also let me iterate without climbing back into the ceiling. During the migration, I moved occupancy classification onto the ESP so the rest of the pipeline no longer had to do it.
Here’s the final ESPHome configuration (which, admittedly, still became much more C++ heavy than anticipated):
esphome:
name: heislberg-wcm
esp32:
board: esp32-poe
framework:
type: arduino
logger:
ota:
password: <OTAPASSWORD>
ethernet:
type: LAN8720
mdc_pin: GPIO23
mdio_pin: GPIO18
clk_mode: GPIO17_OUT
phy_addr: 0
power_pin: GPIO12
mqtt:
broker: <MQTTSERVER>
binary_sensor:
- platform: gpio
pin:
number: GPIO34
inverted: true
id: button_onboard
############################ MAGNET SENSOR ############################
i2c:
sda: GPIO13
scl: GPIO16
scan: false
sensor:
- platform: hmc5883l
id: door_sensor
field_strength_x:
name: "Field Strength X"
filters:
- delta: 1.5
on_raw_value:
then:
- lambda: 'id(current_x) = x;'
field_strength_y:
name: "Field Strength Y"
filters:
- delta: 3.0
on_raw_value:
then:
- lambda: 'id(current_y) = x;'
field_strength_z:
name: "Field Strength Z"
filters:
- delta: 1.0
on_raw_value:
then:
- lambda: 'id(current_z) = x;'
- script.execute: update_door_state
oversampling: 4x
range: 130uT
update_interval: 0.2s
############################ SENSOR POSTPROCESSING ############################
text_sensor:
- platform: template
id: door_state
name: "Door State"
filters:
lambda: |-
if (x == id(door_state).state) {
return {};
} else {
return x;
}
globals:
- id: current_x
type: float
restore_value: no
initial_value: '0'
- id: current_y
type: float
restore_value: no
initial_value: '0'
- id: current_z
type: float
restore_value: no
initial_value: '0'
script:
- id: update_door_state
then:
- lambda: !lambda |-
// static declarations
struct xyz {
std::string name;
float x;
float y;
float z;
xyz(std::string cname, float cx, float cy, float cz) {
name = cname;
x = cx;
y = cy;
z = cz;
}
};
xyz positions[] = {
xyz("open", -45.2, -96.0, 0.1),
xyz("closed", -47.3, -101.6, 0.1),
xyz("locked", -52.0, -125.6, 0.1)
};
// actual updating code
xyz current("current", id(current_x), id(current_y), id(current_z));
float smallest_distance = std::numeric_limits<float>::max();
xyz nearest = xyz("none", 0, 0, 0);
for (xyz position: positions) {
float dx = current.x - position.x;
float dy = current.y - position.y;
float dz = current.z - position.z;
float distance = std::sqrt(dx * dx + dy * dy + dz * dz);
if (distance < smallest_distance) {
smallest_distance = distance;
nearest = position;
}
}
id(door_state).publish_state(nearest.name);
MQTT server#
The MQTT broker was a standard Mosquitto server in our Kubernetes cluster, deployed specifically for this project (because of course we spun up dedicated infrastructure for toilet monitoring).
The broker ran on the guest network alongside the ESP32, which kept the toilet traffic separated from production systems and made the network admin happy. It handled only a handful of topics, but Kubernetes gave the toilet enterprise-grade message queuing and automatic restarts. Priorities.
Janus: Data Processing#
Before migrating to ESPHome, we kept classification off the ESP so changing magnetic field thresholds would not require reflashing it and spelunking through the ceiling again.
So we wrote Janus, a small Go microservice that acts as the central hub for all toilet data:
- Subscribes to raw XYZ magnetic field values
- Classifies these positions in 3D space (open/closed/locked) using thresholds
- Exposes toilet status via HTTP endpoints
- Reports metrics to Datadog (because we needed toilet analytics, apparently)
- Runs in Kubernetes for high availability (obviously)
The name? Janus is the Roman god of doors and transitions. Also, it sounds like… well, you get it. We’re professionals here.
As described above, we later moved classification onto the ESP, but Janus remained responsible for data format conversions and the API endpoints. Never waste a good pun-based microservice name!
The Many Visualizations#
Company Dashboard#
We added the toilet metrics to our main Datadog dashboard, right between API response times and Kubernetes pod health. The Datadog gauge was a bit slow to react though, so we hacked together a small dashboard embed fed by Janus for real-time updates. A highlight at every weekly tech sync.
Menu Bar Apps#
I also wrote a small Swift app for macOS that lived in the menu bar. A tiny toilet icon with either a red or green dot. It could even send notifications when the toilet became free!

The app used CocoaMQTT to subscribe to the heislberg/door_state topic and mapped each state to an emoji. The fun part: You could configure it to run shell commands on state changes, so you could theoretically hook it up to anything - notifications, scripts, whatever automation your heart desired.
If you by chance ever happen to need a macOS menu bar app that shows toilet occupancy, feel free to use it:
Random ESPs#
I had a few spare Wemos D1 mini boards lying around, so I flashed them with a simple ESPHome config that showed the toilet status on the small onboard LED - off for free, on for occupied. Dangling on their small USB cables, we put them in various places around the office - under desks, on shelves, wherever someone might glance at them and get a quick status update.
The ON AIR Sign#
Lastly, I had grand plans for a huge “ON AIR” sign that would light up when occupied. You know, the classic red broadcasting light that every podcast studio has - except ours would indicate a very different kind of live performance.
I’d already sourced everything: a vintage-style LED sign from Amazon, an ESP8266-based relay module from AliExpress to control it, and I’d even started writing the ESPHome config.
Sadly, I left the company before installation day arrived. But, somewhere in my basement, buried in a box of electronics parts, that sign still waits for its moment of glory.
The Legacy#
Before I left, the system had been running flawlessly for months. It processed over 15,000 “transactions.” The average occupancy was 6.3 minutes. Peak usage was Wednesday at 03:30 PM (after the all-hands meeting with a lot of coffee).
The beautiful thing is that the system might still be running. Hidden in the walls, an ESP32 silently monitoring magnetic fields, broadcasting to an MQTT topic that maybe nobody subscribes to anymore. A digital ghost of over-engineering past.
This is Part 4 of my “Unnecessary Home Automation” series. No toilets were harmed in the making of this project. The CEO eventually found out, and found it pretty funny after all. Privacy concerns were… addressed retroactively.

