I like saunas. For those unfamiliar with the concept, it is a hot room where you are occasionally beaten with tree-branches.

You might have seen images like this: Man sitting in a sauna

While in reality it is often more like this

Several men sitting in a sauna

Anyway, while looking for my very own financial burden to call home, I stumbled upon an offering which heavily promoted the fact that it had a sauna.

Neat! Then I found out it had a remote capable controller. Even neater! The only issue is that it uses Huum’s proprietary cloud to do the remote controlling. And I very much dislike that. I mean it is fine for normal people, but I will not use any smartness on any device that I cannot control locally.

But having remote control would still be nice, wouldn’t it?

I’ve been thinking about this on-and-off for over a year now. It’d be oh so nice to arrive home from shit weather and be able to hop into a sauna that has already done the 1.5 hour long heat to 90 degrees.

Theory

Searching the web I found that there’s an official-ish Home Assistant integration for Huum, but it requires logging in with the Huum app before use and basically only replicates the API calls the mobile app does. The integration seems to be built around the pyhuum library.

But I don’t like that. I want my HA to control the controller directly.

So I need to figure out how the controller speaks with the cloud. Or how the cloud speaks with the controller? In both cases the Controller has to be the one initiating the connection as otherwise I would have to allow the outside connection through the firewall. Which I have not (and will not) do. It is way too complicated for a regular person anyway.

After consulting ChatGPT, we arrived at a solution: I was to use port mirroring on my switch to direct all the traffic generated main module to my laptop, where it could be Wiresharked. Crap, it doesn’t do eth, only Wi-Fi. (Uku WiFi…).

I then had the idea of setting my laptop up as a hotspot to connect the controller to. Firstly, it isolates the controller traffic from the rest of my home stuffs, and secondly, it should allow me to MITM the communication.

These are my devices in my network!

Basically, the plan is to use my laptop’s hotspot to sniff traffic between the controller and a known API endpoint https://api.huum.eu/action/.

After writing the previous sentence, two thoughts occurred - the first one is that this is the endpoint receiving commands from the mobile app, but it is not necessarily the one sending commands to my controller. And the second one is that if TLS is in play (which I damn hope is the case), I won’t have access to the request payloads. But you see, request payloads sound like the juicy bits of this ordeal.

Third thought, actually, what if the controller opens a websocket to the cloud? How do I sniff traffic then? I have no experience with websockets…

Ehh, one step at a time.

Gebedee proposes mitmproxy to listen in on the TLS payloads. The docs seem simple enough.

Also, bear in mind that up until this point I’ve not yet actually done anything. The above is pure speculation and research.

Looking into the traffic

So, I’ve set up a hotspot using NetworkManager on my laptop, and am using a wired connection to retain world-wide-web capabilities.

After about 3 hours of wiresharking and looking for ways to read TLS encrypted messages, I stumbled upon a TCP frame with the following payload:

1
0b9900ac68b59c000000000000000000000000000000000000000000000000005551342d342d322e322e313231332d34613164366461312d340000000000555134204555205769466900000000

Now, it might not seem much, but Wireshark also shows the payload converted to ASCII

UQ4-4-2.2.1213-4a1d6da1-4 and UQ4 EU WiFi? That’s not TLS-ed at all! When checking the controller info menu, they seem to be the firmware version and controller’s friendly name. Cool!

Looking more at the traffic, every 4-ish minutes the controller seems to say something in 11 bytes. The red box is the previous handshake.

Payload of those messages is always 09 17 00 f8 23 00 00 00 00 00 00. The only thing I’ve managed to decode from this thus far is the reading from the sauna’s temperature sensor - the second byte 17 seems to indicate the current reading form the temperature sensor at 23 degees.

The first byte of 09 is likely either message type, or some framing byte.

Let’s try turning the sauna on by pressing the button on the controller. I powered on the heater, counted to 20 Mipsipipi, and powered it off again.

First packet to the server at 14:48:36 08 5a 00 00 00 00 03 11 6c 2c 68 41 96 2c 68 13 6c 2c 68 00 00 00 00 01 00

and a second “turn off” packet at 14:49:03 08 5a 00 00 00 00 03 00 00 00 00 00 00 00 00 2e 6c 2c 68 00 00 00 00 01 00

27 seconds apart… close enough.

My idea of the payload contents - byte 08 indicates a “heater control” message. 5a indicates target temp of 90 degrees (I did indeed set target to 90), and then there’s some unknown crap. There are also some groups that look like timestamps:

“Heater on” message:

Hex (little endian)DecISO8601Thoughts
11 6c 2c 681747741713May 20, 2025 11:48:33Button press time?
41 96 2c 681747752513May 20, 2025 14:48:33The controller is set to turn off after 3 hours, so this is expiry time
13 6c 2c 681747741715May 20, 2025 11:48:35Send message to cloud time?

“Heater off” message:

Hex (little endian)DecISO8601Thoughts
2e 6c 2c 681747741742May 20, 2025 11:49:02Turn off time

That’s good to know. Also, I’ve developed a suspicion. The suspicion is that the communication is done using a common protocol on the IoT world, MQTT, because the messages don’t seem to be json or xml which is often used in larger applications.

Right-way remote control

Alright, I now want to see what is said when I toggle the heater using my mobile app, as this-way comms is what I’m really after.

I’ve turned the heater on and set the target temp to 90 degrees. Huum’s server sent 07 5a 00 00 00 00 03 24 76 2c 68 54 a0 2c 68 3d 76 2c 68 ec c5 ef 10 00

The first byte of 07 looks like “Heater control from remote server”, 5a is the target temp of 90 degrees, and then some more timestamps.

The regular ping of 11 bytes also changed: 09 18 00 f8 24 00 00 00 00 00 00 The second byte now reads 18, which happens to be the current temperature sensor reading at 24 degrees!

So I guess I have the parts I need.

Attempting remote control

I noticed that when the controller connects to wifi, it does a DNS lookup for api.huum.eu. I think the easiest option is to direct said domain to a MQTT server I control. Now just gotta figure out what server it could be… (I mean Mosquitto, HiveMQ, etc.)

Now, I’m writing this blog as I go, and when I got to pondering about the suitable MQTT server some things came up and I had to take a break. And during that break I started thinking (weird, right?), MQTT is supposed to be a pub-sub protocol, where parties publish stuff into topics and other different parties subscribe to those topics. But the payloads we’ve thus far extracted are a little too small to contain such info…

What if it’s WebSocket?

Before diving into writing code, I needed to get one other important piece out of the way. Namely, api.huum.eu still pointed to Huum’s actual EC2 instance. To solve this I deployed AdGuard Home, which has an easy way to “rewrite” a DNS record by simply adding 192.168.1.146 api.huum.eu to their Custom filtering solution. I know I technically could have messed around with dnsmasq in my router, but it’s a hassle and I broke the whole setup last time. AdGuard Home worked amazingly easily.

Now that drill api.huum.eu returns 192.168.1.146, I can move on to writing code. I firstly asked Gebedee to draft a Bun server that can handle websockets, ran it, and saw red lines in Wireshark.

Some errors were about something bad, but some were HTTP/1.1 505 HTTP Version Not Supported. Since it already used HTTP/1.1 then I guess it didn’t like HTTP at all. I don’t know enough about WebSockets off the top of my head to know the link between WS and HTTP. Doesn’t matter.

But what if it’s raw TCP?

Some more messing about later, and it is raw TCP!

I’ve now spent considerable time writing some code and also discovering more message types. One of the neater types is 0x02, which allows setting the frequency with which the controller sends an update about its temperature and heater state. An even neater thing is that this also causes the heater to send out 0x08 and 0x09 messages - former indicating if it is currently heating and the time frames, add the latter info about current temperature. Absolutely Home Assistantable!

Building an actual control system

Now, all this payloading is cool and all, but I want a way to control my heater first using a simple rest api, and then using Home Assistant.

I’ve used Bun to build both the TCP socket and HTTP server. I chose Bun because it is what’s flashy right now, and I don’t like Deno. Node is just too old-school.

Basically from here on out it is quite simple API build work. Endpoints to get and set temperature update frequency, and a pair to start and stop the heater.

The realization

I am at the boring part of this project - making everything pretty. I have more-or-less understood how the TCP communication works, I am successfully reading it, and also starting actions myself; I’ve found a way to force the controller to speak to my server, instead of Huum’s proprietary one.

All that now remains is polishing the server code and pushing to GitHub… But maaan do I not feel like doing it…

Summary

So I’ve figured out five different message types that are used in communication between the controller and the cloud. The flow starts with the controller establishing connection and then sending 0x0B. Cloud then sends a 0x02 to set the update frequency, and also immediately update its knowledge of the heater. The controller responds with 0x09 stating current temperature and heater state, and a 0x08 stating heater target temperature and whether it is currently heating (by specifying heating start and scheduled end times).

Heater control is done via 0x07 messages, and they are used by both the local physical button thing, and sent from the cloud. Once the controller receives the 0x07, it sends back a 0x08 to update cloud’s state. One peculiar thing I’ve noticed is that the 0x08 is sent only then turning off the heater. Refer to the realization as to why I don’t know more about why this is so.

My attempt at a conclusion

Smart home is rapidly gaining popularity and more and more crap is becoming “smart”. While I do like the idea of being able to control my things remotely, unpleasantly often said remote control capability comes with two major drawbacks -

  1. we are required to install the vendor’s proprietary app, that’s often shoddily made and pipes way too much telemetry back home;
  2. the architecture is shoddy and cheaply made, often lacking security measures, or simply lying about what the product does.

So with this avalanche of different apps on our phones, all piping way too much data for what should be needed to control a simple relay back to their authors, inevitable the desire build your own systems arises.

Should one decide to semi-diy their smart home by combining products from different vendors, you’ll quickly end up with 10 different apps, all controlling a single thing. (Hi FIL!)

I’ve yet to take a stance on whether I’m happy over the lack of TLS because it made my life so much easier, or furious that my heater could technically be controlled by someone other than myself (and Huum).

Next steps include figuring out if I can get access to a list of heaters or possibly control one not my own. I know a bloke who has an identical setup and lives not far from me.

Oh, and GitHub too.


Message types and Payloads

0x02 - Set Ping frequency

Message is sent by the server to set how often the sauna controller should report its temperature sensor reading and heater state. Can be used whenever to update the frequency.

Sample Message:

1
02 3f 31 2e 68 fc 00
Byte (group)Meaning
02Message ID - Ping frequency update
3f 31 2e 68Current timestamp (Little Endian)
fc0 - 255 delay in seconds
00Termination

0x07 - Heater control

Used by both the local physical interface and the server to turn the heater on and off.

Sample Message:

1
07 38 00 00 00 00 03 47 2f 2e 68 77 59 2e 68 47 2f 2e 68 ec c5 ef 10 00
Byte (group)Meaning
07Message ID - Heater control
38Current temp in Hex (56deg here)
00 00 00 00Unknown padding
03Unknown
47 2f 2e 68Heating started timestamp
77 59 2e 68Heating stop timestamp
47 2f 2e 68Current timestamp
ec c5 ef 10Unknown values
00Termination

Sample message for turning the heater off

1
07 38 00 00 00 00 03 00 00 00 00 00 00 00 00 65 42 2e 68 75 59 fc 10 00

The “heating started” and “heating stop” timestamps are here zeroed, meaning the heater will turn off.

0x08 - Send update to cloud

Message is used to send heater state changes to the cloud.

Sample Message:

1
08 38 00 00 00 00 03 00 00 00 00 00 00 00 00 3f 31 2e 68 00 00 00 00 01 00
Byte (group)Meaning
08Message ID - Update cloud
38Current temp in Hex (56deg here)
00 00 00 00Unknown padding
03Unknown
47 2f 2e 68Heating started timestamp
77 59 2e 68Heating stop timestamp
47 2f 2e 68Current timestamp
ec c5 ef 10Unknown values
00Termination

Update about heating being stopped follows the same logic as “Heater control”

0x09 - Status ping

Sent by the controller to report temperature sensor reading.

Sample Message:

1
09 21 00 fc 24 00 00 00 00 00 00
Byte (group)Meaning
09Message ID - Status update
21Current temp in Hex (33deg here)
00Padding?
fcFrequency in seconds
24Heater status (23, 24, 25)
00 00 00 00 00Padding?
00Termination

0x0B - Server greeting

Sent by the controller when establishing connection with the cloud server. Contains firmware version, controller’s friendly name, and some other stuff I’m too lazy to figure out.

Sample Message:

1
0b 00 00 ac 68 b5 9c 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 55 51 34 2d 34 2d 32 2e 32 2e 31 32 31 33 2d 34 61 31 64 36 64 61 31 2d 34 00 00 00 00 00 55 51 34 20 45 55 20 57 69 46 69 00 00 00 00