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
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.
Samsung EHS heat pumps have two completely separate communication paths. Understanding the distinction is critical:
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)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)| Identity | What it is |
|---|---|
| Device Cloud ID (DI) | The client app's identity on the CI server (UUID) |
| HP Device ID | The heat pump's OCF device UUID |
| UID (GUID) | Samsung Account user identifier |
| Access Token | Samsung Account OAuth token (~24h expiry) |
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.
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.
┌─────────────────────────┐ │ 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 └─────────────────────────┘
| Hostname | Purpose |
|---|---|
| connects-v2.samsungiots.com | Sign-up entry point |
| ocfclientcon-shard-eu02s-euwest1.samsungiots.com | Client connections |
| ocfconnect-shard-eu02s-euwest1.samsungiots.com | Device connections |
| Method | URI | Purpose |
|---|---|---|
| POST | /oic/account?apiVersion=v2 | Cloud sign-up (register client) |
| POST | /oic/account/session | Cloud sign-in |
| GET | /oic/route/{deviceId}/... | Read device resource (proxied) |
| GET | /oic/res?rt=oic.wk.d | Discover cloud devices |
| GET | /oic/acl/group?members={uid} | List ACL groups |
The CI server requires a Samsung Account OAuth token — not a SmartThings 3rd-party OAuth token. These are two completely separate authentication systems.
| Token | Format | Source |
|---|---|---|
| Samsung Account | HN9Iy...hF4 (~25 chars) | eu-auth2.samsungosp.com |
| SmartThings 3P | ccdb1e97-e35b-... (UUID) | SmartThings OAuth |
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:
GET auth2.samsungosp.com/v2/license/open/whoareyou → regional endpointPOST eu-auth2.samsungosp.com/auth/oauth2/token with auth codegrant_type=refresh_token — 2-year rolling expiry, indefinite accessThe complete flow to connect to Samsung's CI server and access device resources:
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.
Send a CoAP Capabilities and Settings Message (code 225) with Max-Message-Size option. The server may or may not respond — continue either way.
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.
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.
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.
Working Python code for each step of the CI server interaction. These examples use asyncio for networking and cbor2 for payload encoding.
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'')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,
}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 FalseRegister 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}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)
}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 NoneThe 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 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.
The CI server only accepts Samsung Account OAuth tokens (short alphanumeric, ~25 chars). SmartThings installed-app tokens (UUID format) return TOKEN_EXPIRED.
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.
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.
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.
Samsung Account refresh tokens have a 2-year rolling expiry. Each refresh returns a new refresh_token, allowing indefinite token maintenance without re-authentication.
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.
The protocol specification used by Samsung's CI servers. CoAP messages use a different framing than UDP CoAP.
The OCF standard for cloud-mediated device communication. Samsung's implementation follows this closely.
All payloads between client and CI server use CBOR encoding. Python's cbor2 library handles encoding/decoding.
The open-source OCF implementation that Samsung's SmartThings app uses internally. The native layer (libocstack) handles CoAP, DTLS, and resource management.
The official SmartThings REST API. This is the supported 3rd-party integration path, but doesn't expose raw OCF resources.
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.