Shelly 智能 IoT 设备集成开发指南
本文面向希望快速接入 Shelly Cloud 集成器 API 的第三方开发者,涵盖账号申请、设备授权、WebSocket 实时通信、命令下发等完整流程,并附有可直接使用的 TypeScript 示例代码。
目录
- 整体架构概览
- 准备工作:申请集成器账号
- 用户授权流程
- 获取 WebSocket 访问令牌
- 建立 WebSocket 长连接
- 处理实时事件
- 下发设备控制命令
- 回调安全验证
- 完整集成示例(Next.js Route Handler)
- 常见问题与注意事项
1. 整体架构概览
Shelly 集成器 API 采用回调 + WebSocket 双通道模型:
用户浏览器
│ 打开授权页面
▼
Shelly Cloud (my.shelly.cloud)
│ 设备绑定/解绑时,POST 回调到你的服务器
▼
你的后端服务器 ──────► 数据库(存储 userId / deviceId / host 映射)
│
│ 建立 WSS 长连接(每个 Shelly 服务器一条)
▼
Shelly Cloud WSS Server (wss://<HOST>:6113/...)
│ 推送设备状态变更、在线/离线、设置变更事件
│ 接收控制命令并返回响应
▼
你的业务逻辑关键概念:
| 概念 | 说明 |
|---|---|
INTEGRATOR_TAG | 集成器唯一标识,由 Shelly 官方分配 |
INTEGRATOR_TOKEN | 用于换取 JWT 的密钥 |
JWT (Access Token) | 有效期 24 小时,用于 WSS 鉴权 |
HOST | 设备所在 Shelly Cloud 服务器地址,每个用户固定归属某一节点 |
deviceId | 每台 Shelly 设备的唯一 ID |
2. 准备工作:申请集成器账号
向 Shelly 官方申请集成器资质,获得专属的 tag(标识)和 token(密钥):
- 发送邮件至:
support@allterco.com - 或填写表单:https://forms.office.com/e/KDxYr4K3vF
申请成功后,将这两个值保存到你的环境变量中:
# .env
SHELLY_INTEGRATOR_TAG=your_integrator_tag
SHELLY_INTEGRATOR_TAG_TOKEN=your_integrator_token3. 用户授权流程
3.1 生成授权链接
用户需要在 Shelly 官方页面完成设备授权。你需要构造一个包含你的 tag 和回调地址的授权 URL,引导用户访问。
// shelly-utils.ts
export function makeAuthUrl(
options: ShellyIntegratorOptions
): string {
return `https://my.shelly.cloud/integrator.html?itg=${encodeURIComponent(options.tag)}&cb=${encodeURIComponent(options.callbackUrl)}`
}实践建议:在回调 URL 中附加你自己的用户 ID,以便在回调时关联账号:
function generateCallbackUrlForUser(
myCorporationUserId: string
): string {
const base = 'https://your-domain.com/api/shelly-integrator'
return `${base}?myCorporationUserId=${encodeURIComponent(myCorporationUserId)}`
}
// 最终 URL 示例:
// https://my.shelly.cloud/integrator.html?itg=YOUR_TAG&cb=https%3A%2F%2Fyour-domain.com%2Fapi%2Fshelly-integrator%3FmyCorporationUserId%3Duser1233.2 处理授权回调
当用户在授权页面完成设备绑定或解绑操作后,Shelly Cloud 会向你的回调地址发送 POST 请求:
{
"userId": 12345,
"deviceId": "251458041414384",
"deviceType": "light",
"deviceCode": "S3DM-0A101WWL",
"accessGroups": "01",
"action": "add",
"host": "shelly-1-eu.shelly.cloud",
"name": ["Plug 1"]
}关键字段说明:
| 字段 | 说明 |
|---|---|
action | "add" 表示用户授权,"remove" 表示用户撤权 |
host | 必须保存,后续 WebSocket 连接需要用到此地址 |
accessGroups | 访问权限:"00" 只读,"01" 可读写控制 |
deviceId | 设备唯一 ID,后续下发命令时使用 |
⚠️ 注意:回调接口必须返回
200 OK,否则 Shelly Cloud 将中断操作。
核心数据持久化逻辑:
async function bindShellyDeviceUserWithMine(
myCorporationUserId: string,
event: ShellyDeviceEvent
) {
if (event.action === 'add') {
// 将 {myCorporationUserId, shellyUserId, deviceId, host, accessGroups} 存入数据库
await db.shellyDevice.upsert({
where: { deviceId: event.deviceId },
create: {
myCorporationUserId,
shellyUserId: event.userId,
deviceId: event.deviceId,
host: event.host,
accessGroups: event.accessGroups,
},
update: { host: event.host, accessGroups: event.accessGroups },
})
} else if (event.action === 'remove') {
await db.shellyDevice.delete({
where: { deviceId: event.deviceId },
})
}
}4. 获取 WebSocket 访问令牌
JWT 令牌有效期为 24 小时,用于 WebSocket 连接鉴权和 API 调用。
export async function fetchAccessTokenUsedByWebsocket(
options: ShellyIntegratorOptions
): Promise<string> {
const params = new URLSearchParams({
itg: options.tag,
token: options.tagToken,
})
const response = await fetch(
'https://api.shelly.cloud/integrator/get_access_token',
{
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: params.toString(),
}
)
if (!response.ok) throw new Error(`HTTP ${response.status}`)
const data = await response.json()
if (!data.isok) throw new Error(data.error || 'Unknown error')
return data.data // JWT 字符串
}💡 最佳实践:建议在服务端缓存 JWT,并在过期前提前刷新,避免每次连接都重新请求。
5. 建立 WebSocket 长连接
5.1 连接规则
- 连接地址格式:
wss://<HOST>:6113/shelly/wss/hk_sock?t=<JWT> - 每个
HOST最多允许一条活跃连接 - 你的用户可能分布在多个 Shelly 服务器节点,需要为每个节点维护一条 WSS 连接
- 连接一旦建立可以无限期保持,无需频繁重连
function newShellyWebSocket(
shellyUserHost: string,
token: string,
onMessage?: (e: MessageEvent) => void
): WebSocket {
const url = `wss://${shellyUserHost}:6113/shelly/wss/hk_sock?t=${token}`
const ws = new WebSocket(url)
if (onMessage) ws.onmessage = onMessage
ws.onopen = () =>
console.log(`[Shelly WS] Connected to ${shellyUserHost}`)
ws.onclose = (ev) => {
console.warn(
`[Shelly WS] Disconnected from ${shellyUserHost}`,
ev.code,
ev.reason
)
// TODO: 实现重连逻辑(建议指数退避)
}
ws.onerror = (err) => console.error(`[Shelly WS] Error`, err)
return ws
}5.2 多节点连接管理
// 从数据库中获取所有不同的 host,为每个 host 建立一条连接
const hosts = await db.shellyDevice.findMany({
distinct: ['host'],
select: { host: true },
})
const connections = new Map<string, WebSocket>()
for (const { host } of hosts) {
if (!connections.has(host)) {
const ws = newShellyWebSocket(host, jwtToken, handleShellyEvent)
connections.set(host, ws)
}
}5.3 Example 代码
/**
* Example demonstrating a typical Shelly websocket usage:
* - registers a lightweight event handler
* - connects to the websocket for the provided user host and token
* - sends a sample command payload
* https://shelly-api-docs.shelly.cloud/integrator-api/communication
* This function is intended as documentation and example code and is not exported.
* @param {string} shellyUserHost Shelly cloud host for the user (hostname without scheme), get if from db (bind shelly device user with your user in callback, and save host into db for fetch when connect websocket), or from ShellyDeviceEvent.host when receive callback
* @param {string} token Access token (JWT) used for websocket authentication, get it from `fetchAccessTokenUsedByWebsocket`
*/
function exampleShellyWebsocketUsage(
shellyUserHost: string,
token: string
) {
const onShellyWebsocketEventListen = (e: MessageEvent) => {
const shellyEvent = e.data?.event || ''
switch (shellyEvent) {
case 'Shelly:Settings':
//todo:
break
case 'Shelly:Online':
//todo::
break
case 'Shelly:StatusOnChange':
//todo::
break
default:
//commands and rpc' response will be there
console.debug('Received Shelly event', shellyEvent, e.data)
}
}
const ws = newShellyWebSocket(
shellyUserHost,
token,
onShellyWebsocketEventListen
)
const commandOrRpcPayload =
ShellyBuildEventPayload.CommandRequest.Relay(
'on',
'251458041414384',
60
)
ws.send(commandOrRpcPayload)
//do not release/free ws until you are done with sending commands or listening to events
}6. 处理实时事件
Shelly Cloud 通过 WebSocket 推送以下几类事件:
6.1 事件类型
| 事件名 | 触发时机 |
|---|---|
Shelly:StatusOnChange | 设备状态变化(开/关、功率等) |
Shelly:Settings | 设备配置变更 |
Shelly:Online | 设备上线/下线 |
Shelly:CommandResponse | 控制命令的执行结果 |
Integrator:ActionResponse | ActionRequest 的响应 |
6.2 事件处理示例
function handleShellyEvent(e: MessageEvent) {
let payload: any
try {
payload = JSON.parse(e.data)
} catch {
console.error('Failed to parse Shelly event', e.data)
return
}
switch (payload.event) {
case 'Shelly:StatusOnChange':
// payload.deviceId, payload.status
console.log(
`设备 ${payload.deviceId} 状态更新:`,
payload.status
)
// TODO: 更新数据库或推送给前端
break
case 'Shelly:Online':
// payload.online: 1 = 在线, 0 = 离线
console.log(
`设备 ${payload.deviceId} ${payload.online ? '上线' : '下线'}`
)
break
case 'Shelly:Settings':
console.log(
`设备 ${payload.deviceId} 配置变更:`,
payload.settings
)
break
case 'Shelly:CommandResponse':
// payload.trid 对应请求的事务 ID
console.log(`命令响应 [trid=${payload.trid}]:`, payload.data)
break
default:
console.debug('未知事件', payload.event, payload)
}
}7. 下发设备控制命令
所有命令通过 WebSocket 发送 JSON 字符串。每条命令包含一个 trid(事务 ID),用于匹配异步响应。
7.1 继电器/插座控制(Relay)
// 打开继电器,60 秒后自动关闭
const payload = JSON.stringify({
event: 'Shelly:CommandRequest',
trid: Math.floor(Math.random() * 999),
deviceId: '251458041414384',
data: {
cmd: 'relay',
params: { id: 0, turn: 'on', timeout: 60 },
},
})
ws.send(payload)7.2 灯光控制(Light)
const payload = JSON.stringify({
event: 'Shelly:CommandRequest',
trid: Math.floor(Math.random() * 999),
deviceId: 'DEVICE_ID',
data: {
cmd: 'light',
params: { id: 0, turn: 'on', mode: 'white', temp: 4000 },
},
})
ws.send(payload)7.3 卷帘/百叶窗控制(Roller)
// 基础开/关/停
const rollerPayload = JSON.stringify({
event: 'Shelly:CommandRequest',
trid: Math.floor(Math.random() * 999),
deviceId: 'DEVICE_ID',
data: {
cmd: 'roller',
params: { id: 0, go: 'open' }, // "open" | "close" | "stop"
},
})
// 精确位置控制(需设备已完成校准)
const rollerPosPayload = JSON.stringify({
event: 'Shelly:CommandRequest',
trid: Math.floor(Math.random() * 999),
deviceId: 'DEVICE_ID',
data: {
cmd: 'roller_to_pos',
params: { id: 0, pos: 50 }, // 移动到 50% 位置
},
})7.4 温控器设置(JRPC)
const jrpcPayload = JSON.stringify({
event: 'Shelly:JrpcRequest',
trid: Math.floor(Math.random() * 999),
deviceId: 'DEVICE_ID',
method: 'Thermostat.SetConfig',
params: { id: 0, config: { target_C: 22 } }, // 设置目标温度为 22°C
})
ws.send(jrpcPayload)7.5 命令响应处理
{
"event": "Shelly:CommandResponse",
"trid": 123,
"deviceId": "251458041414384",
"user": 12345,
"data": {
"isok": true,
"res": "OK"
}
}常见错误结果:
| result 值 | 说明 |
|---|---|
OK | 执行成功 |
UNAUTHORIZED | 无控制权限(需检查 accessGroups) |
WRONG_API_PARAMETERS | 请求参数格式错误 |
WRONG_HOST | 设备不在当前服务器,响应中会附带正确的 host |
8. 回调安全验证
每次 Shelly Cloud 调用你的回调接口时,都会附带一个 SCL-Trust HTTP 请求头,内含 ES384 签名的 JWT,用于证明请求来自 Shelly 官方。
验证方法:
import JWT from 'jsonwebtoken'
const SHELLY_PUBLIC_KEY = `-----BEGIN PUBLIC KEY-----
MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAE3Kx+6C/0ZbnelYUgucUo4/X4xt1NCmEL
coyLpgkuLHume4VLZnQjtXeYgzr2FUdsO/ip8SzssSu3CEU9ArvB+yGIlW7l1yLt
wHVs/2zXrL0riL++7jdoQCpTGanFVzpM
-----END PUBLIC KEY-----`
export function isCallbackSecurityTokenValid(
securityToken: string
): boolean {
try {
JWT.verify(securityToken, SHELLY_PUBLIC_KEY, {
algorithms: ['ES384'],
})
return true
} catch (err) {
console.warn('无效的回调安全令牌', err)
return false
}
}JWT 的 payload 结构如下:
{
"exp": 1712345678, // UNIX 时间戳,有效期 2 分钟
"itg": "your_integrator_tag",
"did": "device_id"
}🔒 安全说明:此令牌有效期仅 2 分钟,且绑定了特定设备 ID 和集成器 TAG,可有效防止重放攻击。
9. 完整集成示例(Next.js Route Handler)
以下是一个完整的 Next.js API Route 示例,整合了上述所有流程:
// app/api/shelly-integrator/route.ts
import { NextResponse, NextRequest } from 'next/server'
import {
ShellyDeviceEvent,
makeAuthUrl,
fetchAccessTokenUsedByWebsocket,
ShellyIntegratorOptions,
isCallbackSecurityTokenValid,
} from './shelly-utils'
import { config } from '@/lib/config/envs'
export const dynamic = 'force-dynamic'
// 生成包含当前用户 ID 的回调 URL
function generateCallbackUrl(myCorporationUserId: string): string {
const base = 'https://your-domain.com/api/shelly-integrator'
return `${base}?myCorporationUserId=${encodeURIComponent(myCorporationUserId)}`
}
// GET /api/shelly-integrator
// 返回用于前端引导用户授权的链接,以及 WebSocket 令牌
export async function GET() {
const authedUserId = '从 session/cookie 中获取当前用户 ID'
const options: ShellyIntegratorOptions = {
tag: config.SHELLY_INTEGRATOR_TAG,
tagToken: config.SHELLY_INTEGRATOR_TAG_TOKEN,
callbackUrl: generateCallbackUrl(authedUserId),
}
const [forUserOpenAuthorizationWebPageURL, websocketToken] =
await Promise.all([
makeAuthUrl(options),
fetchAccessTokenUsedByWebsocket(options),
])
return NextResponse.json({
websocketToken, // 可在前端或后端使用
forUserOpenAuthorizationWebPageURL, // 引导用户打开此链接进行授权
})
}
// POST /api/shelly-integrator?myCorporationUserId=xxx
// Shelly Cloud 设备授权/解除授权回调
export async function POST(request: NextRequest) {
// 1. 验证请求合法性
const securityToken = request.headers.get('SCL-Trust') || ''
if (!isCallbackSecurityTokenValid(securityToken)) {
return NextResponse.json(
{ error: 'Invalid security token' },
{ status: 401 }
)
}
// 2. 获取当前用户 ID(来自回调 URL 的 query param)
const myCorporationUserId =
request.nextUrl.searchParams.get('myCorporationUserId') || ''
// 3. 解析设备事件
const shellyDeviceEvent: ShellyDeviceEvent = await request.json()
// 4. 存储/删除设备与用户的绑定关系
if (shellyDeviceEvent.action === 'add') {
// TODO: await db.save({ myCorporationUserId, ...shellyDeviceEvent })
} else if (shellyDeviceEvent.action === 'remove') {
// TODO: await db.delete({ deviceId: shellyDeviceEvent.deviceId })
}
// 5. 必须返回 200 OK,否则 Shelly Cloud 将中断操作
return NextResponse.json({ success: true })
}10. 常见问题与注意事项
Q1:一个集成器需要建立几条 WebSocket 连接?
Shelly 设备按用户归属分配到不同的云服务器节点(host 不同)。你的集成器需要为每个有活跃用户的节点各维护一条 WSS 连接。同一节点只允许一条活跃连接。
Q2:JWT 过期后 WebSocket 会断开吗?
不会。连接建立后可以无限期保持。但每次新建连接时,都需要使用有效的 JWT(有效期 24 小时)。建议在 JWT 过期前主动刷新并存储备用。
Q3:收到 WRONG_HOST 错误怎么办?
说明设备已迁移到其他服务器节点。从错误响应中取出新的 host,更新数据库记录,并确保已为新 host 建立 WSS 连接,然后重发命令。
{
"event": "Integrator:ActionResponse",
"data": {
"result": "WRONG_HOST",
"deviceId": "251458041414384",
"host": "shelly-2-eu.shelly.cloud"
}
}Q4:如何主动解除设备绑定?
调用 Shelly Cloud 的设备取消订阅接口:
curl -X POST https://<HOST>.shelly.cloud/integrator/unsubscribe_device \
-H 'Authorization: Bearer <JWT>' \
-d id=<DEVICE_ID>⚠️ 注意:解除绑定后,需要你自己通知用户。
Q5:设备的 accessGroups 为 "00" 时能下发命令吗?
不能。"00" 表示只读权限,只能接收设备状态事件,无法控制设备。只有 accessGroups 包含控制位(如 "01")时,才能发送控制命令。
快速集成清单
- [ ] 向 Shelly 申请集成器
tag和token - [ ] 配置环境变量
SHELLY_INTEGRATOR_TAG和SHELLY_INTEGRATOR_TAG_TOKEN - [ ] 实现回调接口(POST),并处理
add/remove事件 - [ ] 在回调中保存
deviceId → host映射到数据库 - [ ] 实现 JWT 获取与缓存(24 小时有效期)
- [ ] 为每个不同的
host建立 WebSocket 连接并处理断线重连 - [ ] 实现
SCL-TrustHeader 的 ES384 签名验证 - [ ] 根据业务需求,实现 relay / light / roller / JRPC 等控制命令