Reverse Engineering the Govee Pool Thermometer: Getting H5109 Data Into Home Assistant

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:

  1. Install: brew install --cask proxyman
  2. Launch Proxyman — it installs its CA certificate on the Mac automatically
  3. On iPhone: set WiFi proxy to the Mac’s IP on port 9090
  4. Navigate to the URL Proxyman shows to download the iOS profile
  5. Settings → General → About → Certificate Trust Settings → enable full trust for Proxyman CA
  6. 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.