Reverse Engineering the Govee Pool Thermometer: Getting H5109 Data Into Home Assistant
March 12, 2026
The Govee H5109 is a floating pool thermometer that pairs with an H5042 WiFi gateway hub. Temperature shows up in the Govee app just fine. Getting it into Home Assistant? That’s a different story.
No existing integration supports the H5109. Not govee2mqtt. Not the HA Govee BLE integration. Not the official Govee API (my developer key application has been pending for weeks). So I intercepted the app’s traffic and built my own.
This post covers the full process: why BLE doesn’t work, how to capture the Govee cloud API with a MITM proxy, and a 50-line bash script that gets your pool temperature into Home Assistant via MQTT auto-discovery.
Why Nothing Off-the-Shelf Works
govee2mqtt
I was already running govee2mqtt in Docker for my other Govee thermometers (H5074, H5075). It connects to Govee’s cloud via AWS IoT and maps devices to MQTT entities. The H5109 shows up in the logs, but that’s as far as it goes:
H5109_0049 (03:1F:74:29:00:00:00:0C:FF:FF:00:3C:FF:FF:00:49 H5109)
Undoc: room=None supports_iot=false ble_only=true
Unknown device type. Cannot map to Home Assistant.
The device is flagged ble_only=true by Govee’s internal API. govee2mqtt sees it, shrugs, and moves on.
Home Assistant Govee BLE Integration
My Intel NUC has a Bluetooth 5.4 USB dongle about ten feet from the pool through a glass door. Three other Govee thermometers work perfectly over BLE. So I tried adding the H5109 to the Govee BLE integration. No devices found.
I dug into the integration source inside the HA container:
/usr/local/lib/python3.14/site-packages/govee_ble/parser.py
The parser (v0.44.0) supports H5072, H5074, H5075, H5100 through H5108, H5110, H5174, H5177 through H5179, H5051, H5052, H5071, H5181 through H5185, H5198, H5055, and H5106. The H5109 is conspicuously absent from that list.
But even if it were supported, it wouldn’t matter.
The Real Problem: 433MHz, Not Bluetooth
After digging through GitHub issues, HA community forums, and the govee2mqtt codebase, I found the actual architecture:
The H5109 floating sensor talks to the H5042 gateway over 433MHz radio, not Bluetooth.
H5109 (pool float) --[433MHz]--> H5042 (WiFi hub) --[WiFi]--> Govee Cloud --> Govee App
The Bluetooth radio on the H5109 is only used for initial pairing with the hub. It never broadcasts temperature data over BLE. No BLE-based integration will ever see pool temperatures because there are no pool temperatures on the 2.4GHz band.
The only path to the data is through Govee’s cloud.
The MITM Approach
Since the cloud API is the only option and the official developer key is stuck in purgatory, I decided to intercept the traffic from the Govee iOS app. My device, my phone, my network.
Setting Up Proxyman
Proxyman is a macOS HTTPS proxy that can decrypt TLS traffic from your phone by installing a trusted CA certificate.
Setup takes about five minutes:
- Install:
brew install --cask proxyman - Launch Proxyman — it installs its CA certificate on the Mac automatically
- On iPhone: set WiFi proxy to the Mac’s IP on port 9090
- Navigate to the URL Proxyman shows to download the iOS profile
- Settings → General → About → Certificate Trust Settings → enable full trust for Proxyman CA
- In Proxyman, add an SSL Proxying rule for
app2.govee.com
Capturing the API
I opened the Govee app, navigated to the pool thermometer, and Proxyman lit up with requests. The interesting one was the device list endpoint:
POST https://app2.govee.com/device/rest/devices/v1/list
Headers:
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...
Content-Type: application/json
clientId: 511f8b5744854ef8b79d36522457b087
appVersion: 7.3.20
clientType: 1
Body: {}
An empty body. You POST nothing and get everything. The response contains every device on your account, and buried in the H5109 entry:
{
"device": "03:1F:74:29:00:00:00:0C:FF:FF:00:3C:FF:FF:00:49",
"sku": "H5109",
"deviceName": "POOL_TEMP",
"deviceExt": {
"lastDeviceData": "{\"online\":false,\"tem\":2762,\"hum\":0,\"lastTime\":1773280800000}"
}
}
tem: 2762 is 27.62°C — divide by 100. The lastTime is Unix milliseconds. online: false just means the float isn’t actively transmitting at that instant; it sends readings periodically to conserve battery.
That’s the whole API. One endpoint, empty body, Bearer token in the header.
Authentication Details
| Header | Value | Notes |
|---|---|---|
Authorization |
Bearer <JWT> |
App session token |
clientId |
511f8b5744854ef8b79d36522457b087 |
App installation ID |
appVersion |
7.3.20 |
Govee app version |
clientType |
1 |
1 = iOS |
Decoding the JWT payload reveals the token expires in roughly 60 days. That’s generous enough for a practical integration — you’ll need to re-intercept a fresh token every couple of months until the official API key comes through.
Other Endpoints Captured
The device settings endpoint returned some additional metadata:
POST https://app2.govee.com/device/rest/devices/v1/settings
Body: {
"device": "03:1F:74:29:00:00:00:0C:FF:FF:00:3C:FF:FF:00:49",
"sku": "H5109"
}
Response includes battery level, temperature calibration offset, min/max range (10.00°C to 32.00°C), and the gateway link confirming the H5042 hub relationship. Useful for a future battery sensor but not critical for temperature.
There’s also a historical data endpoint (/th/v2/data-tasks) that returns temperature logs as downloadable zip files — overkill for live monitoring.
Building the Integration
A 50-line bash script in an Alpine Docker container. No Python frameworks, no custom HA components, no Rust compile times. Just curl, jq, and mosquitto_pub.
The Script
#!/bin/bash
# Poll Govee cloud API for H5109 pool temperature, publish to HA via MQTT
MQTT_HOST="127.0.0.1"
MQTT_PORT="1883"
AUTH_TOKEN="<your JWT token>"
DEVICE_ID="03:1F:74:29:00:00:00:0C:FF:FF:00:3C:FF:FF:00:49"
while true; do
RESPONSE=$(curl -s -X POST "https://app2.govee.com/device/rest/devices/v1/list" \
-H "Authorization: Bearer $AUTH_TOKEN" \
-H "Content-Type: application/json" \
-H "clientId: 511f8b5744854ef8b79d36522457b087" \
-H "appVersion: 7.3.20" \
-H "clientType: 1" \
-d "{}")
DEVICE_DATA=$(echo "$RESPONSE" | jq -r \
".devices[] | select(.device == \"$DEVICE_ID\") | .deviceExt.lastDeviceData")
if [ -n "$DEVICE_DATA" ] && [ "$DEVICE_DATA" != "null" ]; then
TEM_RAW=$(echo "$DEVICE_DATA" | jq -r '.tem')
ONLINE=$(echo "$DEVICE_DATA" | jq -r '.online')
TEMP_C=$(echo "scale=1; $TEM_RAW / 100" | bc)
TEMP_F=$(echo "scale=1; $TEMP_C * 9 / 5 + 32" | bc)
# MQTT auto-discovery config (retained, published once per loop)
mosquitto_pub -h $MQTT_HOST -p $MQTT_PORT \
-t "homeassistant/sensor/pool_temp/config" -r -m '{
"name": "Pool Temperature",
"unique_id": "govee_h5109_pool_temp",
"state_topic": "govee/pool_temp/state",
"unit_of_measurement": "°F",
"device_class": "temperature",
"value_template": "{{ value_json.temperature_f }}",
"json_attributes_topic": "govee/pool_temp/state",
"device": {
"identifiers": ["govee_h5109_pool"],
"name": "Pool Thermometer",
"manufacturer": "Govee",
"model": "H5109"
}
}'
# Publish state
mosquitto_pub -h $MQTT_HOST -p $MQTT_PORT \
-t "govee/pool_temp/state" -r \
-m "{\"temperature_c\": $TEMP_C, \"temperature_f\": $TEMP_F, \"online\": $ONLINE}"
echo "$(date): Pool temp: ${TEMP_C}°C / ${TEMP_F}°F (online: $ONLINE)"
else
echo "$(date): Failed to get pool temp data"
fi
sleep 300
done
Why MQTT Auto-Discovery
The script publishes a discovery config to homeassistant/sensor/pool_temp/config with every loop iteration. Home Assistant picks this up and automatically creates the entity with the correct device class, units, and device grouping. No manual configuration.yaml editing, no restart required. The retained flag (-r) ensures the last known value survives HA restarts.
Docker Setup
FROM alpine:3.21
RUN apk add --no-cache curl jq mosquitto-clients bc bash
COPY pool_temp_poller.sh /app/pool_temp_poller.sh
RUN chmod +x /app/pool_temp_poller.sh
CMD ["/app/pool_temp_poller.sh"]
# docker-compose.yml addition
pool-temp:
build:
context: .
dockerfile: Dockerfile.pool-temp
container_name: pool-temp
network_mode: host
restart: unless-stopped
environment:
- TZ=America/New_York
The final image is about 6MB. After docker compose up -d --build pool-temp, the entity appeared in HA within seconds:
sensor.pool_thermometer_pool_temperature = 81.6°F
The Full Data Path
H5109 (pool float)
↓ 433MHz radio
H5042 (WiFi gateway hub)
↓ WiFi → Internet
Govee Cloud (app2.govee.com)
↓ HTTPS (polled every 5 min)
pool-temp container (NUC)
↓ MQTT
Mosquitto → Home Assistant
→ sensor.pool_thermometer_pool_temperature
Five hops from a floating thermometer to a dashboard entity. Not elegant, but it works.
Lessons Learned
Not everything is Bluetooth. The H5109 uses 433MHz to reach its hub. Just because a device lists Bluetooth in its specs doesn’t mean it sends sensor data over BLE. The H5109 uses BT only for initial pairing — all temperature data goes through the 433MHz link to the WiFi gateway and up to the cloud.
Mobile app MITM is a 15-minute setup. Proxyman on macOS with an iOS device takes about five minutes to configure, then you’re watching every API call the app makes in real time. If an IoT device has a mobile app, it has a cloud API. The API is just undocumented.
JWT tokens from mobile apps are generous. The Govee token lives for roughly 60 days. Mobile app developers don’t want users to re-login constantly, so tokens tend to be long-lived. That’s a security trade-off for them and a convenience for us.
Sometimes the simplest solution wins. A bash script with curl, jq, and mosquitto_pub in a 6MB Alpine container solved what no existing integration could handle. The temptation is always to build a proper Python component with async I/O and entity platforms and config flows. But for a single sensor that updates every five minutes, a shell loop is the right tool.
MQTT auto-discovery is underappreciated. Publish a correctly formatted JSON config to homeassistant/sensor/<name>/config and the entity materializes in HA with device grouping, units, and icons. No YAML. No restarts. It’s the fastest path from “I have data” to “I can see it in my dashboard.”
What’s Next
The token renewal is the weak link — every 60 days, I’ll need to fire up Proxyman and grab a fresh JWT from the app. Once Govee processes my developer API key application, I can switch to the official API with proper OAuth. Until then, this works perfectly.
The battery level from the settings endpoint would be a nice addition. And if you’re running this with multiple Govee devices, the /v1/list call returns all of them — you could extend the script to poll any device Govee supports. But for now, I have a pool temperature in Home Assistant, and that’s what I set out to get.