Reverse Engineering the Gulfstream Compass WiFi Pool Heater: A Complete API Guide for Home Assistant

If you own a Gulfstream pool heat pump with the Compass WiFi controller, you’ve probably experienced the app. The reviews on Google Play tell the story: 1.71 stars, connection issues, no temperature history, no third-party integration. The Compass WiFi system is a walled garden — your pool heater data goes into their cloud and the only way to get it out is through their mediocre app.

I wanted my pool heater in Home Assistant. Gulfstream doesn’t offer an API. Nobody online has documented one. So I decided to find it myself.

This post walks through the entire reverse engineering process — from pulling apart the Android APK to a fully documented, working API with curl examples you can run right now. If you have a Gulfstream pool heater with Compass WiFi, this is everything you need to integrate it into Home Assistant, Node-RED, or any other home automation platform.

The Target

The Compass WiFi system consists of a small antenna module that plugs into the control board of post-May-2022 Gulfstream heat pumps. The module connects to your home WiFi (2.4GHz only) and communicates with a cloud server. The Compass app on your phone talks to the same cloud server to display status and send commands. The antenna doesn’t expose any local API — everything goes through the cloud.

The app is published by ICM Controls Corp (package name: com.icmcontrols.gulfstream), a company that makes HVAC control boards. Gulfstream is their pool heater brand. The app has roughly 1,000 downloads on Google Play, which tells you how niche this ecosystem is.

My goal was simple: find the cloud API endpoints, understand the authentication, map the data fields to actual physical meanings, and build a Home Assistant integration.

Step 1: Getting the APK

The Compass app is available on both iOS and Android. Android is the easier target for reverse engineering because APK files are essentially ZIP archives containing compiled Java/Kotlin code that can be decompiled back to readable source.

I downloaded the APK from APKPure (version 1.0.12). The file was actually an XAPK — a container format that bundles the base APK with additional split APKs. Extracting the XAPK is straightforward:

# XAPK is just a ZIP
unzip "Compass Wifi Heat Pump Navigat_1.0.12_APKPure.xapk" -d compass_extracted

Inside, I found the base APK along with configuration and resource splits. The interesting stuff is in the base APK.

Step 2: Decompilation

For Android APKs, jadx is the go-to decompiler. It converts Dalvik bytecode back into readable Java source. However, this particular app turned out to be built with Xamarin — Microsoft’s cross-platform mobile framework that uses C#/.NET instead of Java/Kotlin. This meant the actual application logic was compiled into .NET DLL assemblies rather than Dalvik bytecode.

# Install jadx for initial inspection
brew install jadx

# Decompile the APK
jadx -d compass_decompiled compass_base.apk

The jadx output confirmed the Xamarin architecture — the Java layer was mostly boilerplate, with the real logic in assemblies/ inside the APK. The DLL files needed a different tool.

For .NET decompilation, I extracted the DLL assemblies and used monodis (part of the Mono framework) to disassemble them:

# Extract assemblies from the APK
unzip compass_base.apk "assemblies/*" -d extracted_dlls

# Decompile with monodis
monodis --output=GulfstreamHeatPump.il assemblies/GulfstreamHeatPump.dll

The decompiled output wasn’t as clean as what jadx produces for native Android apps, but it was more than enough to find what I needed.

Step 3: Finding the API

With the decompiled source in hand, I started grepping for the obvious targets:

# Find URLs and endpoints
grep -ri "http" --include="*.il" --include="*.cs" --include="*.java" -l
grep -ri "api" --include="*.il" --include="*.cs" -l

# Find auth-related code
grep -ri "login\|password\|token\|auth" --include="*.il" --include="*.cs"

The first hit was gold. In the decompiled GulfstreamHeatPump assembly, I found references to a base URL:

https://www.captouchwifi.com/icm/api/call

Everything goes through this single endpoint. Every API call is an HTTP POST with a JSON body that includes an action field specifying what you want to do. This is a classic RPC-style API pattern — simple, but effective.

Further digging through the decompiled code revealed the complete list of supported actions, their parameters, and the field names used in the protocol. The app’s code was reasonably organized, making it straightforward to map out the entire API surface.

Step 4: Testing the API

With the endpoint and action names in hand, I started making real API calls. The first test was authentication:

curl -s -X POST 'https://www.captouchwifi.com/icm/api/call' \
  -H 'Content-Type: application/json;charset=utf-8' \
  -d '{"action":"login","username":"YOUR_USER","password":"YOUR_PASS"}'

The response came back immediately:

{
  "action": "login",
  "result": "success",
  "token": "f6509640bb6bf0370505ca7a6288a3b3"
}

No OAuth. No API keys. No rate limiting. No certificate pinning. Just username, password, and a 32-character hex token that appears to be long-lived. Classic small-company IoT security.

With the token, I could list my devices:

curl -s -X POST 'https://www.captouchwifi.com/icm/api/call' \
  -H 'Content-Type: application/json;charset=utf-8' \
  -d '{"action":"getPasDevices","token":"YOUR_TOKEN","additionalFields":"special parameter holder"}'

Yes, that "additionalFields":"special parameter holder" is a required parameter. Some developer’s placeholder string that shipped to production. It has to be included verbatim or the call fails.

The response returned my heat pump with its unique key, online status, and metadata. From there, thermostatGetDetail gave me the full device state — over 180 fields of register data.

Step 5: The Hard Part — Field Mapping

This is where the real work began. The API returns field names like MD, HTS, LCS, RMT, RSV1, GEN15, and GEN9. The decompiled code provided some hints about what these abbreviations meant, but the labels were frequently misleading.

For example, the code suggested:

These names turned out to be wrong or misleading for actual usage. The only way to verify was empirical testing — changing values in the Compass app and immediately polling the API to see which fields changed, then cross-referencing the values with what the physical heat pump was doing.

Here’s what I found after extensive testing:

HTS is NOT the heat setpoint. It’s the evaporator sensor calibration offset, using an offset-10 encoding where the stored value equals the actual offset plus 10. So HTS=10 means 0°F calibration, HTS=9 means -1°F.

RMT IS the water temperature. Despite being labeled “Remote TSTAT” in the code, this field contains the live pool water temperature reading in °F.

RSV1 is the actual heat setpoint. When I changed the target temperature in the app from 85°F to 86°F, RSV1 changed from 85 to 86.

GEN15 is the coil temperature. Not LCS as the code suggested.

GEN9 is a fault/status bitfield where individual bits indicate specific conditions — bit 2 (value 4) means “no flow,” and bit 4 (value 16) means “heating call active.”

RMH indicates compressor status — 0 means off, 128 means running.

Without this empirical verification, any integration built from the decompiled code alone would have displayed wrong data for nearly every sensor. The register names in the firmware are abbreviations that made sense to the ICM Controls engineer who wrote the code, not to anyone reading them from outside.

The Complete API

Here’s everything you need to control your Gulfstream pool heater programmatically.

Base URL

All requests are HTTP POST to:

https://www.captouchwifi.com/icm/api/call

Content-Type: application/json;charset=utf-8

Authentication

curl -s -X POST 'https://www.captouchwifi.com/icm/api/call' \
  -H 'Content-Type: application/json;charset=utf-8' \
  -d '{
    "action": "login",
    "username": "YOUR_EMAIL",
    "password": "YOUR_PASSWORD"
  }'

Returns a token string used in all subsequent calls.

List Devices

curl -s -X POST 'https://www.captouchwifi.com/icm/api/call' \
  -H 'Content-Type: application/json;charset=utf-8' \
  -d '{
    "action": "getPasDevices",
    "token": "YOUR_TOKEN",
    "additionalFields": "special parameter holder"
  }'

Returns an array of devices with unique_key (format: 0000CP######), name, model, and online status. The additionalFields parameter with that exact string value is required.

Get Device Status

curl -s -X POST 'https://www.captouchwifi.com/icm/api/call' \
  -H 'Content-Type: application/json;charset=utf-8' \
  -d '{
    "action": "thermostatGetDetail",
    "thermostatKey": "YOUR_DEVICE_KEY",
    "token": "YOUR_TOKEN"
  }'

Returns the full device state in detail.currentState with 180+ fields.

Set Device Fields

# Turn on Pool Heat mode
curl -s -X POST 'https://www.captouchwifi.com/icm/api/call' \
  -H 'Content-Type: application/json;charset=utf-8' \
  -d '{
    "action": "thermostatSetFields",
    "thermostatKey": "YOUR_DEVICE_KEY",
    "token": "YOUR_TOKEN",
    "fields": {"MD": "1"}
  }'

# Set temperature to 86°F
curl -s -X POST 'https://www.captouchwifi.com/icm/api/call' \
  -H 'Content-Type: application/json;charset=utf-8' \
  -d '{
    "action": "thermostatSetFields",
    "thermostatKey": "YOUR_DEVICE_KEY",
    "token": "YOUR_TOKEN",
    "fields": {"RSV1": "86"}
  }'

# Turn off
curl -s -X POST 'https://www.captouchwifi.com/icm/api/call' \
  -H 'Content-Type: application/json;charset=utf-8' \
  -d '{
    "action": "thermostatSetFields",
    "thermostatKey": "YOUR_DEVICE_KEY",
    "token": "YOUR_TOKEN",
    "fields": {"MD": "0"}
  }'

You can set multiple fields in a single call by adding them to the fields object.

Get Alerts

curl -s -X POST 'https://www.captouchwifi.com/icm/api/call' \
  -H 'Content-Type: application/json;charset=utf-8' \
  -d '{
    "action": "thermostatGetAlerts",
    "thermostatKey": "YOUR_DEVICE_KEY",
    "token": "YOUR_TOKEN"
  }'

Verified Field Reference

Live Sensor Readings

Field Description Unit Verified
RMT Pool water temperature °F Yes — matches app display
GEN15 Coil temperature °F Yes — matches app display
RMH Compressor status (0=off, 128=running) Yes
GEN9 Fault/status bitfield (see below) Yes
GEN13 Status flag (16 when heating active) Observed
LCS Unknown sensor reading (changes frequently) Observed

Fault/Status Bitfield (GEN9)

Bit Value Meaning Verified
0 1 Evaporator sensor malfunction From app code
1 2 Water sensor malfunction From app code
2 4 No flow Yes — present when pump off
3 8 Low pressure switch From app code
4 16 Heating call active Yes — present when MD=1

Example: GEN9 = 20 means 16 + 4 = heating active + no water flow.

Mode and Setpoint

Field Description Values Verified
MD Operating mode 0=Off, 1=Pool Heat Yes
RSV1 Heat temperature setpoint 50–104 °F Yes
RSV2 Cool temperature setpoint (probable) °F Not yet verified
DFU Deadband °F Yes
LKD Panel lock 0=unlocked, 1=locked From app code
SCH Schedule mode 0=off From app code

Operational Settings

Field Description Encoding Verified
HTS Evaporator sensor calibration actual = value − 10 Yes
DB Water sensor calibration actual = value − 10 Yes
CAL Anti short-cycle delay minutes, direct Yes
AXD Defrost end temperature °F, direct Yes
MXH Maximum heat setpoint limit °F From device
MNH Minimum heat setpoint limit °F From device

Calibration Offset Encoding

Two fields use an offset-10 encoding: HTS (evaporator calibration) and DB (water sensor calibration). The stored value is the actual calibration offset plus 10. So:

All other fields use direct values with no conversion.

Additional API Actions (Untested)

Action Key Parameters Description
thermostatSetModeData thermostatKey, token, modeData Set mode with schedule data
thermostatSetBlock thermostatKey, token, startAddress, length, data Write register block
thermostatSetAlert thermostatKey, token, alertType, value, enabled Configure alert
thermostatSetAlertMethod thermostatKey, token, email, mobile, text Set notification method
thermostatSetShareWith thermostatKey, token, shareWith Share device control
thermostatGetShareWith thermostatKey, token List shared users
addThermostat thermostatKey, zipcode, name, description, token Add new device
removeThermostat thermostatKey, token Remove device

Building a Home Assistant Integration

With this API documented, building a custom Home Assistant integration is straightforward. The minimum viable integration needs:

  1. Authentication coordinator — login, store token, re-authenticate on failure
  2. Data update coordinator — poll thermostatGetDetail every 60 seconds
  3. Climate entity — maps MD (mode), RSV1 (setpoint), and RMT (current temperature)
  4. Sensors — water temp (RMT), coil temp (GEN15), compressor status (RMH)
  5. Binary sensors — online status, fault detection (GEN9 != 0), lock state (LKD)

The API is simple enough that the entire integration can be built around Python’s aiohttp library making POST requests to the single endpoint. No WebSocket connections, no MQTT, no complex auth flows.

I’m working on a complete Home Assistant custom component and will publish it on GitHub once it’s tested and stable. If you build something with this API in the meantime, I’d love to hear about it.

Lessons Learned

Register names lie. Every initial assumption from the decompiled code turned out to be wrong for the important fields. HTS wasn’t the setpoint, LCS wasn’t the coil temp, RMT wasn’t a remote thermostat. If I’d built the integration from decompilation alone without empirical testing, every sensor would have displayed the wrong value. Always verify against the physical hardware.

Small IoT companies don’t do security. No API keys, no OAuth, no certificate pinning, no rate limiting, plain HTTP POST with JSON, long-lived tokens, and a magic string parameter that some developer forgot to remove. This isn’t a criticism — for a pool heater with 1,000 app users, spending engineering time on API security would be irrational. But it does make reverse engineering trivially easy.

Xamarin apps are slightly harder to decompile than native Android. The .NET DLL assemblies require different tools than standard Android APKs. But the fundamental approach is the same: extract the archive, find the compiled binaries, decompile them, and grep for interesting strings.

The gap between “closed ecosystem” and “fully documented API” can be about an hour. I went from “this device has no integration and never will” to a complete, tested API specification in a single evening. If you have an IoT device with a mobile app and no published API, the API almost certainly exists — it’s just undocumented. The app has to talk to something.

If you’re a Gulfstream pool heater owner who’s been frustrated by the Compass app, I hope this helps. And if you work at ICM Controls and you’re reading this — maybe publish an official API? The community would appreciate it. Also, you might want to remove “special parameter holder” from production.