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:
While in reality it is often more like this
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:
|
|
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) | Dec | ISO8601 | Thoughts |
---|---|---|---|
11 6c 2c 68 | 1747741713 | May 20, 2025 11:48:33 | Button press time? |
41 96 2c 68 | 1747752513 | May 20, 2025 14:48:33 | The controller is set to turn off after 3 hours, so this is expiry time |
13 6c 2c 68 | 1747741715 | May 20, 2025 11:48:35 | Send message to cloud time? |
“Heater off” message:
Hex (little endian) | Dec | ISO8601 | Thoughts |
---|---|---|---|
2e 6c 2c 68 | 1747741742 | May 20, 2025 11:49:02 | Turn 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 -
- we are required to install the vendor’s proprietary app, that’s often shoddily made and pipes way too much telemetry back home;
- 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:
|
|
Byte (group) | Meaning |
---|---|
02 | Message ID - Ping frequency update |
3f 31 2e 68 | Current timestamp (Little Endian) |
fc | 0 - 255 delay in seconds |
00 | Termination |
0x07 - Heater control
Used by both the local physical interface and the server to turn the heater on and off.
Sample Message:
|
|
Byte (group) | Meaning |
---|---|
07 | Message ID - Heater control |
38 | Current temp in Hex (56 deg here) |
00 00 00 00 | Unknown padding |
03 | Unknown |
47 2f 2e 68 | Heating started timestamp |
77 59 2e 68 | Heating stop timestamp |
47 2f 2e 68 | Current timestamp |
ec c5 ef 10 | Unknown values |
00 | Termination |
Sample message for turning the heater off
|
|
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:
|
|
Byte (group) | Meaning |
---|---|
08 | Message ID - Update cloud |
38 | Current temp in Hex (56 deg here) |
00 00 00 00 | Unknown padding |
03 | Unknown |
47 2f 2e 68 | Heating started timestamp |
77 59 2e 68 | Heating stop timestamp |
47 2f 2e 68 | Current timestamp |
ec c5 ef 10 | Unknown values |
00 | Termination |
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:
|
|
Byte (group) | Meaning |
---|---|
09 | Message ID - Status update |
21 | Current temp in Hex (33 deg here) |
00 | Padding? |
fc | Frequency in seconds |
24 | Heater status (23 , 24 , 25 ) |
00 00 00 00 00 | Padding? |
00 | Termination |
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:
|
|