Reverse Engineering the Samsung EHS Cloud

A technical deep-dive into how Samsung EHS heat pumps communicate with the cloud — the OCF protocol, CoAP over TLS, CI server authentication, and what we've learned trying to read device data directly.

Last updated: February 2026

Overview

Samsung EHS heat pumps expose far more data through their native OCF (Open Connectivity Foundation) interface than what's available via the SmartThings REST API. The water law offset, detailed temperature readings, energy metrics, and installer settings are all accessible through OCF — but only if you can speak the right protocol.

This article documents our investigation into Samsung's cloud infrastructure, from protocol capture through to working Python code. We can authenticate, register clients, and sign in to Samsung's CI (Cloud Infrastructure) servers — and when authorized, read full device resource data via CoAP.

Current status: We can authenticate to the CI server but cannot access heat pump devices due to missing ACL group associations. The group linking a client to a device is created during SmartThings app onboarding and cannot currently be replicated externally. See The Authorization Problem for details.

How Samsung EHS Cloud Works

Samsung EHS heat pumps have two completely separate communication paths. Understanding the distinction is critical:

Path 1: SmartThings REST API (3rd-party access)

The standard SmartThings API that Dwellsee and other integrations use. Supports reading device status, sending commands, and subscribing to events. Limited to capabilities Samsung has explicitly exposed.

Your Server → SmartThings REST API → Samsung Cloud → Heat Pump
              ↑                                            ↓
         JSON response                              CoAP proxy
   (limited capabilities)                    (full OCF resource data)

Path 2: OCF Cloud / CI Server (native access)

The SmartThings app's native IoTivity stack connects directly to Samsung's CI server via CoAP over TLS (port 443). This gives access to the full OCF resource tree including water law offset, detailed temperatures, and settings not available via REST.

SmartThings App → TLS/TCP → CI Server → CoAP proxy → Heat Pump
                            ↑                          ↓
                    CoAP-over-TCP              Full CBOR resource
                    (RFC 8323)               (all OCF attributes)

Key identities

IdentityWhat it is
Device Cloud ID (DI)The client app's identity on the CI server (UUID)
HP Device IDThe heat pump's OCF device UUID
UID (GUID)Samsung Account user identifier
Access TokenSamsung Account OAuth token (~24h expiry)

SmartThings REST API

The SmartThings REST API is the supported 3rd-party integration path. It exposes device capabilities as JSON and supports commands via the execute capability. However, many OCF resources are not mapped to SmartThings capabilities.

What's available vs what's missing

Available via REST API

  • Operating mode (heating/cooling/DHW)
  • Target & current temperatures
  • Power consumption reports
  • FSV settings (via execute)
  • Device status events

Only via OCF / CoAP

  • Water law offset (read-back)
  • Detailed flow/return temperatures
  • Compressor frequency & inverter data
  • Diverter valve position
  • Full resource attribute sets

OCF Cloud Protocol (CoAP over TLS)

Samsung's CI servers speak CoAP over TLS/TCP (RFC 8323) on port 443. This is a binary protocol — not HTTP — using CBOR-encoded payloads.

Protocol stack

┌─────────────────────────┐
│   CBOR Payload          │  Application data
├─────────────────────────┤
│   CoAP (RFC 7252)       │  GET/POST/PUT, options, tokens
├─────────────────────────┤
│   TCP Framing (RFC 8323)│  Variable-length header
├─────────────────────────┤
│   TLS 1.2               │  Server-only or mutual TLS
├─────────────────────────┤
│   TCP                   │  Port 443
└─────────────────────────┘

CI server endpoints

HostnamePurpose
connects-v2.samsungiots.comSign-up entry point
ocfclientcon-shard-eu02s-euwest1.samsungiots.comClient connections
ocfconnect-shard-eu02s-euwest1.samsungiots.comDevice connections

CoAP URIs

MethodURIPurpose
POST/oic/account?apiVersion=v2Cloud sign-up (register client)
POST/oic/account/sessionCloud sign-in
GET/oic/route/{deviceId}/...Read device resource (proxied)
GET/oic/res?rt=oic.wk.dDiscover cloud devices
GET/oic/acl/group?members={uid}List ACL groups

Samsung Account Authentication

The CI server requires a Samsung Account OAuth token — not a SmartThings 3rd-party OAuth token. These are two completely separate authentication systems.

Token types

TokenFormatSource
Samsung AccountHN9Iy...hF4 (~25 chars)eu-auth2.samsungosp.com
SmartThings 3Pccdb1e97-e35b-... (UUID)SmartThings OAuth

OAuth flow

Samsung Account tokens are obtained through Samsung's own OAuth server. The initial login requires the Samsung Account SDK (Chrome Custom Tab with encrypted parameters). After that, tokens can be refreshed indefinitely:

  1. 1Domain discovery: GET auth2.samsungosp.com/v2/license/open/whoareyou → regional endpoint
  2. 2Login: Samsung Account signInGate in Chrome Custom Tab (encrypted svcParam)
  3. 3Token exchange: POST eu-auth2.samsungosp.com/auth/oauth2/token with auth code
  4. 4Refresh: Same endpoint with grant_type=refresh_token — 2-year rolling expiry, indefinite access

CI Server Sign-In Flow

The complete flow to connect to Samsung's CI server and access device resources:

1

TLS Connect

Open a TLS connection to the CI server on port 443. Server-only TLS — no client certificate needed for sign-up or first sign-in.

2

CSM Exchange

Send a CoAP Capabilities and Settings Message (code 225) with Max-Message-Size option. The server may or may not respond — continue either way.

3

Cloud Sign-Up (first time only)

POST to /oic/account?apiVersion=v2 with CBOR payload containing DI, devicetype, clientid, authprovider, accesstoken, and uid. Returns a TLS client certificate, redirect URI (shard), and server ID.

4

Cloud Sign-In

Open a NEW TCP connection (sign-in on the same connection as sign-up times out). POST to /oic/account/session with uid, di, accesstoken, and login:true. Returns expiresin and refreshtoken_expiresin.

5

Read Resources

GET /oic/route/{deviceId}/{resource_path} to read device resources through the CI server's routing proxy. Requires ACL group membership linking your DI to the target device.

Code Examples

Working Python code for each step of the CI server interaction. These examples use asyncio for networking and cbor2 for payload encoding.

CoAP-over-TCP Message Encoding (Python)

Samsung's CI server uses CoAP over TLS/TCP (RFC 8323), not UDP. This encoder handles the variable-length header format.

import struct

def encode_option(delta, value):
    """Encode a single CoAP option."""
    vlen = len(value)
    d = delta if delta < 13 else (13 if delta < 269 else 14)
    d_ext = (b'' if delta < 13
             else struct.pack('B', delta - 13) if delta < 269
             else struct.pack('>H', delta - 269))
    l = vlen if vlen < 13 else (13 if vlen < 269 else 14)
    l_ext = (b'' if vlen < 13
             else struct.pack('B', vlen - 13) if vlen < 269
             else struct.pack('>H', vlen - 269))
    return struct.pack('B', (d << 4) | l) + d_ext + l_ext + value


def encode_coap_tcp(code, token=b'', options=None, payload=b''):
    """Encode a CoAP-over-TCP message (RFC 8323).

    Args:
        code: CoAP method code (1=GET, 2=POST, 225=CSM)
        token: Message token bytes
        options: List of (option_number, value_bytes) tuples
        payload: CBOR payload bytes
    """
    if options is None:
        options = []
    opt_bytes = b''
    prev = 0
    for num, val in options:
        opt_bytes += encode_option(num - prev, val)
        prev = num
    tkl = len(token)
    body_len = len(opt_bytes) + (1 + len(payload) if payload else 0)
    if body_len < 13:
        hdr = struct.pack('BB', (body_len << 4) | tkl, code)
    elif body_len < 269:
        hdr = struct.pack('BBB', (13 << 4) | tkl, body_len - 13, code)
    elif body_len < 65805:
        hdr = struct.pack('>BHB', (14 << 4) | tkl, body_len - 269, code)
    else:
        hdr = struct.pack('>BIB', (15 << 4) | tkl, body_len - 65805, code)
    return hdr + token + opt_bytes + (b'\xff' + payload if payload else b'')

CoAP-over-TCP Message Parser (Python)

Parse incoming CoAP-over-TCP messages. Handles the variable-length header and extracts code, token, and CBOR payload.

import struct

def parse_coap_tcp(data):
    """Parse a CoAP-over-TCP message.

    Returns dict with 'code' (e.g. '2.04'), 'token', 'payload',
    and 'total_len' (for parsing multiple messages from a buffer).
    """
    if len(data) < 2:
        return None
    first = data[0]
    ln = (first >> 4) & 0xF
    tkl = first & 0xF
    off = 1
    if ln < 13:
        ml = ln
    elif ln == 13:
        ml = data[off] + 13; off += 1
    elif ln == 14:
        ml = struct.unpack('>H', data[off:off+2])[0] + 269; off += 2
    else:
        ml = struct.unpack('>I', data[off:off+4])[0] + 65805; off += 4
    code = data[off]; off += 1
    tok = data[off:off+tkl]; off += tkl
    rem = data[off:off+ml]
    pay = b''
    for i in range(len(rem)):
        if rem[i] == 0xFF:
            pay = rem[i+1:]
            break
    return {
        'code': f"{(code >> 5) & 7}.{code & 0x1F:02d}",
        'token': tok,
        'payload': pay,
        'total_len': off + ml,
    }

CI Server Cloud Sign-In (Python)

Complete example: connect to Samsung's CI server over TLS, exchange CSM capabilities, and authenticate with Samsung Account credentials.

import asyncio
import ssl
import struct
import cbor2

CI_HOST = "ocfclientcon-shard-eu02s-euwest1.samsungiots.com"
CI_PORT = 443


async def cloud_signin(uid, device_id, access_token):
    """Sign in to Samsung CI server via CoAP-over-TLS.

    Args:
        uid: Samsung Account GUID (e.g. '7wbtx1jj6j')
        device_id: Client device cloud ID (UUID)
        access_token: Samsung Account OAuth access token
    """
    # TLS connection (server-only, no client cert needed)
    ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
    ctx.check_hostname = False
    ctx.verify_mode = ssl.CERT_NONE

    reader, writer = await asyncio.open_connection(
        CI_HOST, CI_PORT, ssl=ctx
    )

    # Step 1: CSM (Capabilities and Settings Message)
    # Option 2 = Max-Message-Size, value = 1152
    csm = encode_coap_tcp(225, options=[
        (2, struct.pack('>I', 1152))
    ])
    writer.write(csm)
    await writer.drain()
    try:
        await asyncio.wait_for(reader.read(16384), timeout=3.0)
    except asyncio.TimeoutError:
        pass  # CSM response may not come; continue anyway

    # Step 2: Cloud Sign-In
    # POST /oic/account/session
    # Options: 11=Uri-Path, 12=Content-Format (60=CBOR)
    payload = cbor2.dumps({
        "uid": uid,
        "di": device_id,
        "accesstoken": access_token,
        "login": True,
    })
    signin_msg = encode_coap_tcp(2, token=b'\x01', options=[
        (11, b'oic'), (11, b'account'), (11, b'session'),
        (12, struct.pack('>H', 60)),  # application/cbor
    ], payload=payload)
    writer.write(signin_msg)
    await writer.drain()

    # Parse response
    data = await asyncio.wait_for(reader.read(16384), timeout=10.0)
    msg = parse_coap_tcp(data)
    if msg and msg['code'] == '2.04':
        result = cbor2.loads(msg['payload'])
        print(f"Sign-in OK! Expires in {result['expiresin']}s")
        return True

    print(f"Sign-in failed: {msg['code'] if msg else 'no response'}")
    writer.close()
    await writer.wait_closed()
    return False

CI Server Cloud Sign-Up (Python)

Register a new client device with Samsung's CI server. Returns a TLS client certificate, redirect URI, and server ID. Required before first sign-in.

import struct
import uuid
import cbor2


async def cloud_signup(uid, access_token, ci_host, ci_port=443):
    """Register a new client device with the CI server.

    Args:
        uid: Samsung Account GUID
        access_token: Samsung Account OAuth access token
        ci_host: CI server hostname
        ci_port: CI server port (default 443)

    Returns dict with certificate, redirecturi, sid, etc.
    """
    device_id = str(uuid.uuid4())  # generate fresh client ID

    # Connect and exchange CSM (same as sign-in)
    ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
    ctx.check_hostname = False
    ctx.verify_mode = ssl.CERT_NONE
    reader, writer = await asyncio.open_connection(
        ci_host, ci_port, ssl=ctx
    )
    writer.write(encode_coap_tcp(225, options=[
        (2, struct.pack('>I', 1152))
    ]))
    await writer.drain()
    try:
        await asyncio.wait_for(reader.read(16384), timeout=3.0)
    except asyncio.TimeoutError:
        pass

    # POST /oic/account?apiVersion=v2
    # Option 15 = Uri-Query
    payload = cbor2.dumps({
        "di": device_id,
        "devicetype": "samsungconnect",
        "clientid": "6iado3s6jc",
        "authprovider": "https://eu-auth2.samsungosp.com",
        "accesstoken": access_token,
        "uid": uid,
    })
    writer.write(encode_coap_tcp(2, token=b'\x01', options=[
        (11, b'oic'), (11, b'account'),
        (12, struct.pack('>H', 60)),  # application/cbor
        (15, b'apiVersion=v2'),       # URI-Query
    ], payload=payload))
    await writer.drain()

    data = await asyncio.wait_for(reader.read(16384), timeout=15.0)
    msg = parse_coap_tcp(data)
    result = cbor2.loads(msg['payload'])

    # Response includes:
    #   certificate  - PEM client cert (issued by CI server)
    #   redirecturi  - shard URL for future connections
    #   sid          - server ID
    #   accesstoken  - CI server access token
    #   expiresin    - token expiry in seconds

    writer.close()
    await writer.wait_closed()
    return {**result, "di": device_id}

Samsung Account Token Refresh (Python)

Refresh Samsung Account OAuth tokens. The refresh token has a 2-year rolling expiry, so tokens can be maintained indefinitely.

import requests

TOKEN_URL = "https://eu-auth2.samsungosp.com/auth/oauth2/token"
CLIENT_ID = "6iado3s6jc"


def refresh_samsung_token(refresh_token):
    """Exchange a Samsung Account refresh token for fresh tokens.

    The refresh token has a ~2 year rolling expiry (63,072,000 seconds).
    Each refresh returns a new refresh_token, so tokens can be
    maintained indefinitely.

    Returns:
        dict with access_token, refresh_token, userId (GUID),
        expires_in, etc.
    """
    resp = requests.post(TOKEN_URL, data={
        "grant_type": "refresh_token",
        "refresh_token": refresh_token,
        "client_id": CLIENT_ID,
    })
    resp.raise_for_status()
    data = resp.json()
    return {
        "access_token": data["access_token"],
        "refresh_token": data["refresh_token"],
        "uid": data["userId"],           # Samsung Account GUID
        "expires_in": data["expires_in"], # ~86400 (24 hours)
    }

Read OCF Resource via CoAP Route (Python)

After sign-in, read a device resource through the CI server's routing proxy. The CI server forwards the CoAP GET to the heat pump and returns the response.

import cbor2


async def read_ocf_resource(writer, reader, device_id, resource_path):
    """Read an OCF resource from a heat pump via CI server routing.

    Args:
        writer: asyncio StreamWriter (already signed in)
        reader: asyncio StreamReader
        device_id: Heat pump OCF device UUID
        resource_path: e.g. ['temperatures', 'indoor', 'vs', '0']

    Returns:
        dict of resource attributes, or None on failure.

    Example response for /temperatures/indoor/vs/0:
    {
        'x.com.samsung.da.offset': '0.0',
        'x.com.samsung.da.desiredTemp': 27.9,
        'x.com.samsung.da.currentTemp': 22.5,
        'x.com.samsung.da.minTemp': 15.0,
        'x.com.samsung.da.maxTemp': 55.0,
        'x.com.samsung.da.tempIncrement': 0.5,
        'x.com.samsung.da.tempUnit': 'C',
    }
    """
    # Build URI path: /oic/route/{device_id}/{resource_path}
    options = [
        (11, b'oic'), (11, b'route'),
        (11, device_id.encode()),
    ]
    for segment in resource_path:
        options.append((11, segment.encode()))

    # CoAP GET (code=1)
    writer.write(encode_coap_tcp(1, token=b'\x03', options=options))
    await writer.drain()

    try:
        data = await asyncio.wait_for(reader.read(16384), timeout=15.0)
    except asyncio.TimeoutError:
        return None

    msg = parse_coap_tcp(data)
    if msg and msg['payload']:
        return cbor2.loads(msg['payload'])
    return None

The Authorization Problem

This is the unsolved piece. Even with valid authentication, a client can only access devices that share an ACL group with its Device Cloud ID.

How ACL groups work

Samsung Account (UID: 7wbtx1jj6j)
  │
  ├── SmartThings App DI (4598babd-...)
  │     └── ACL Group (created during device onboarding)
  │           ├── SmartThings App DI
  │           └── Heat Pump DI (fe9e0fd1-...)   ← the device
  │
  └── Our DI (freshly registered)
        └── (no groups, no device associations)  ← BLOCKED

What we tried

  • POST /oic/acl/group with various payloads → GROUP_INVALID_PARAM
  • POST /oic/account with HP device ID → USER_INVALID_PARAM
  • Multiple CI server shards (eu01s, eu02s, ocfconnect, ocfclientcon) → HP device not found on any
  • Using the emulator's original DI → DEVICE_NOT_FOUND (old CI server IPs unreachable)

Why it's hard

ACL groups are created by the SmartThings backend during device onboarding (EasySetup). When a user adds a heat pump in the SmartThings app:

  1. The app registers its DI with the CI server (cloudSignUp)
  2. The heat pump registers its own DI during initial setup
  3. The SmartThings backend creates an ACL group containing both DIs
  4. From then on, the app can discover and access the heat pump

Step 3 happens server-side and cannot be triggered via CoAP. The group creation API is internal to Samsung's infrastructure.

Key Findings

No mutual TLS required for initial connection

confirmed

The CI server uses server-only TLS. No client certificate is needed for cloudSignUp or first cloudSignIn. The CI server issues a client certificate during sign-up for subsequent connections.

Samsung Account tokens work from any machine

confirmed

Samsung Account OAuth tokens obtained via refresh_token grant work for CI server authentication from any IP/machine. There's no device binding on the token itself.

SmartThings 3rd-party OAuth tokens don't work

confirmed

The CI server only accepts Samsung Account OAuth tokens (short alphanumeric, ~25 chars). SmartThings installed-app tokens (UUID format) return TOKEN_EXPIRED.

Any UUID works as a client Device Cloud ID

confirmed

The CI server accepts any UUID v4 during cloudSignUp. You don't need to reproduce the UUID v5 derived from the Android ID — the DI is just a client identifier.

Device access requires ACL group membership

blocked

After sign-in, the client can only access devices that share an ACL group with its DI. Groups are created during device onboarding in the SmartThings app and cannot be created via CoAP.

CI server shard affinity

confirmed

Devices register on specific CI server shards. The redirect URI from cloudSignUp indicates the shard. A device registered on one shard is invisible from another.

Token refresh provides indefinite access

confirmed

Samsung Account refresh tokens have a 2-year rolling expiry. Each refresh returns a new refresh_token, allowing indefinite token maintenance without re-authentication.

Full OCF resource reads work (when authorized)

confirmed

With a valid group association, CoAP GET via /oic/route/{deviceId}/... returns full CBOR resource data including water law offset, temperatures, and all device attributes not available via the REST API.

Resources & Further Reading

Can you help?

If you've had success reading OCF resources from Samsung EHS heat pumps, or know how to create CI server ACL group associations, we'd love to hear from you. Open an issue or pull request on our GitHub repository.