Two Months Later: The Raspberry Pi Sauna Controller Caught a Burnt Heating Element

Two Months Later: The Raspberry Pi Sauna Controller Caught a Burnt Heating Element

Two months ago I wrote about the Raspberry Pi sauna controller I built for our rental sauna in Dziwiszow. A $34 Pi, a handful of sensors, a relay chain into the 9kW Huum Drop heater, and a web dashboard my parents could check from their phones.

The honest test of any DIY project is not whether it works on day one. It is whether it still works two months later, with two non-technical users, in February weather, with paying guests showing up every other day.

Short version: it works. The longer version has a burnt heating element, a slightly wrong replacement, and a few features I did not expect to add.

1. The parents test — does it actually replace the manual workflow?

Before the dashboard, my parents managed the sauna by walking down to the cabin, flipping the heater on, and walking back later to check if the room was warm. If a booking moved, somebody had to remember. If somebody forgot, the guests got a cold sauna and a discount.

The new workflow is one bookmark. They open it on their phone. They see:

  • Whether the next booking is in 30 minutes or 5 hours
  • Current sauna temperature and target temperature
  • Whether the heater is on right now, and if so, for how long
  • Any active alerts in red at the top

That is the entire daily interaction for them. Preheating starts automatically based on the booking calendar. They do not touch the heater unless they want to.

The point of automation is the boring case. On a cold winter evening with guests booked, the controller calculates the preheat time itself, switches the heater on at the right moment, and the room is hot when the guests open the door. My parents do not need to know that any of that happened. They just see "next booking in 2 hours, heater on, 64°C climbing" and close the bookmark.

2. The burnt heating element — what the dashboard caught first

In late April I added a Shelly Pro 3EM on the heater feed. The original plan was simple energy logging — how many kWh per session, how much per booking. Cost monitoring stuff.

The Huum Drop 9kW splits its load across three phases — three heating elements, each 3kW. With all three healthy, the Shelly reads a clean 9.0kW of active power while the relay is closed. I logged this number for two weeks just to know what "normal" looks like.

Then one day the dashboard threw a critical alert and an SMS landed on my dad's phone. Total power had dropped well below the 6.5kW threshold, with one of the three phases reading essentially zero. The other two phases were fine. From outside there was no sign anything was wrong — the room still heated, just slower, and the temperature curve looked a little flatter than the calibrated heating curve predicted. Without per-phase power monitoring this would have shown up weeks later as "the sauna takes too long to preheat now" — and we would have blamed the cold weather.

The detection logic is unspectacular and worth sharing. The Pi reads a_act_power, b_act_power, c_act_power from the Shelly every few seconds while the relay is on. It keeps a rolling 3-minute buffer and computes:

  • If total observed power drops below 6.5kW for more than 6 contiguous samples while the relay is on, raise heater-element-burnt (one of the 3kW elements gone).
  • Below 3.5kW: heater-multiple-elements (two elements gone).
  • Below 100W with relay on: heater-no-power (probably the relay or SSR, not the elements).

Each of those three IDs is on an SMS allow-list with a 30-minute cooldown, so the system does not spam my dad's phone if the heater cycles. The cooldown is enforced through the same dismissed-alerts table the dashboard UI uses — same plumbing, two consumers.

The slightly wrong replacement

This is the part that made me laugh. My dad had a spare heating element in the garage. He swapped it in, and the heater fired up across all three phases again. Problem solved. Except the spare he had was a 1.5kW element instead of a 3kW one.

So the heater is now a slightly asymmetric 3 + 3 + 1.5kW machine, 7.5kW total instead of 9. Which is also fine. I bumped the EXPECTED_W constant in the Pi code from 9000 to 7500 to match the new normal, redeployed, and the alert system started watching the right baseline from the next session forward. The heating curve recalibrates from real data on its own.

If I had built this with a commercial controller I would have either (a) never noticed the burnt element, or (b) noticed it and had to manually retune everything for the new wattage. The whole point of doing it yourself is that adaptation is a one-line config change, not a service call.

3. Architecture pivot — Pi stopped answering the door

The original design had the Vercel-hosted dashboard call into the Pi directly over the Tailscale network whenever it needed fresh state. That worked, but it had two problems:

  1. Every dashboard refresh was a network round-trip across two networks. Slow.
  2. If the Pi was momentarily busy reading a sensor, the dashboard would time out and show a stale error.

The new model: the Pi pushes its full state to a Vercel endpoint every 30 seconds, and the response body contains any queued commands waiting for it. So the Pi is the only one initiating connections. Vercel never has to reach into the Pi at all.

Before: Dashboard → Vercel → Pi (over Tailscale) → sensors

After: Pi → Vercel (every 30s, with state)  ·  Vercel returns commands in the response

Sauna Tuula system architecture diagram showing three nodes (Raspberry Pi, Vercel Next.js app, Turso libSQL database), data flow with Pi pushing state every 30 seconds, data models in Prisma schema, code component map, burnt-element detection thresholds, notification channels (SMS/email/web push), and infrastructure layout
The full system map — three nodes (Pi, Vercel, Turso), with the Pi pushing state every 30s and pulling queued commands back in the response.

Side effect: the dashboard now feels snappier because every page load reads cached state from Turso (our SQLite-flavored production DB) instead of waiting on the Pi. Two cache layers — in-memory on the serverless function, and the database row as the persistent fallback. If the Pi has not checked in for 90 seconds the UI shows a "Pi offline" banner automatically.

This pattern — peripheral pushes, central queues commands — is something I should have done from day one. If you are building something similar, skip the "dashboard talks to Pi directly" stage and go straight here.

4. Presence detection and door sensors — finally wired into the logic

In the original article these were sitting on the Pi GPIO doing nothing useful. They are now integrated:

  • HLK-LD2410C radar in the vestibule — the dashboard knows whether anyone has actually entered. If a booking is active but the radar has seen nobody for 25 minutes, the heater shuts itself off. Pure no-show handling: it stops us burning 7.5kW into an empty room because somebody forgot to cancel.
  • Reed switch on the main door — two layers. After 30 seconds open while the heater is on, the Pi cuts the heater (safety). After 1 minute open while heating, a "door open too long" alert appears on the dashboard. Saves a surprising amount of electricity in shoulder seasons when guests prop the door for fresh air.

The radar needed tuning. The sensitivity defaults treated a curtain moving in the draft as a person. I ended up running the sensor in its UART configuration mode and setting per-gate sensitivity manually — once those were calibrated for the vestibule's actual geometry, false triggers stopped.

5. Per-session energy logging — the real cost of one sauna

With the Shelly in place, every heating session now gets logged as one row:

  • Start time, end time, total minutes running
  • kWh used (integrated from the active power readings)
  • Matched booking, if any
  • Sauna temperature when the heater turned on (cold start vs. warm start)
  • Peak temperature reached during the session

[TODO Wojtek — drop in your real numbers: avg PLN/session for 2-hour booking; winter vs. summer delta. I am not going to make these up.]

6. Public booking site talks to the dashboard

The customer-facing booking site at saunatuula.pl now talks to the same database. When a guest books a 6 PM slot, the dashboard sees it immediately — no external sync delay, no manual entry. The Pi then schedules the preheat for that booking automatically.

This was an unplanned consequence of having one source of truth in Turso. Once the booking lives in our DB, everything else — preheat scheduling, SMS confirmations, alert logic — just reads from the same place. The customer site is a thin layer over the same API the internal dashboard uses.

What two months actually proved

The interesting question was never "can a Raspberry Pi control a sauna heater." Lots of people have done that. The question was whether a self-built system, run by two non-technical users on the other end of a phone, would still be working without my intervention after eight weeks of real bookings, weather changes, and a hardware failure.

It does. The burnt element got caught the same day. My parents are not calling me with sauna questions anymore. Every session is itemized in kWh. Recent commits are mostly polish — analytics, voucher options, sensor edge cases — not core sauna logic. The thing is built.

This is our Tuula sauna

The sauna in this post was built from our Tuula plan set — a 4.5 x 3.4m (15 x 11 ft) cabin with a hot room, shower, and changing area for 4-5 people. Full construction drawings, framing details, material lists, and a detailed text description to guide you through the build. $350, digital download.

See the Tuula plan set

Questions about the Shelly integration, the alert logic, or the Pi-push architecture? Drop them in the comments — happy to share more detail on any of it.

Back to blog

Leave a comment

1 of 3