Skip to content

Instantly share code, notes, and snippets.

@Zayrick
Last active May 4, 2025 18:48
Show Gist options
  • Save Zayrick/545ff76535c2f4f5fb382606b93e07b3 to your computer and use it in GitHub Desktop.
Save Zayrick/545ff76535c2f4f5fb382606b93e07b3 to your computer and use it in GitHub Desktop.
小米IoT平台Cloudflare Worker:一个无需本地部署的智能家居设备控制方案,支持设备属性设置、获取和方法调用,适用于各类小米智能设备的远程操作。

小米IoT平台Cloudflare Worker

这是一个用于控制小米智能设备的Cloudflare Worker。它通过小米IoT平台API实现对设备的属性设置、属性获取和方法调用等功能。

功能特点

  • 无需本地运行代理服务器
  • 跨平台支持(可通过HTTP API调用)
  • 支持三种核心操作:设置属性、获取属性和调用方法
  • 自动处理签名验证
  • 支持同时控制/查询多个设备

部署方法

  1. 注册Cloudflare账号并创建一个Worker
  2. _WorkerController-Mijia.js的代码复制到Worker编辑器中
  3. 保存并部署

使用方法

所有请求都需要通过POST方法发送到Worker的URL,并包含以下基本身份验证参数:

{
  "userId": "小米账号ID",
  "serviceToken": "小米服务令牌",
  "deviceId": "通行证设备ID",
  "securityToken": "安全令牌"
}

1. 设置设备属性 (set_properties)

用于控制设备开关、亮度、模式等属性,支持同时控制多个设备或属性。

请求示例:

{
  "userId": "小米账号ID",
  "serviceToken": "小米服务令牌",
  "deviceId": "通行证设备ID",
  "securityToken": "安全令牌",
  "action": "set_properties",
  "params": [
    {
      "did": "设备ID1",
      "siid": 2,
      "piid": 1,
      "value": true
    },
    {
      "did": "设备ID2",
      "siid": 3,
      "piid": 2,
      "value": 50
    }
  ]
}

参数说明:

  • params: 控制参数数组,每项包含:
    • did: 要控制的设备ID
    • siid: 服务ID
    • piid: 属性ID
    • value: 要设置的值(布尔值、数字或字符串)

2. 获取设备属性 (get_property)

用于读取设备的当前状态,支持同时查询多个设备或属性。

请求示例:

{
  "userId": "小米账号ID",
  "serviceToken": "小米服务令牌",
  "deviceId": "通行证设备ID",
  "securityToken": "安全令牌",
  "action": "get_property",
  "params": [
    {
      "did": "设备ID1",
      "siid": 2,
      "piid": 1
    },
    {
      "did": "设备ID2",
      "siid": 3,
      "piid": 2
    }
  ]
}

3. 调用设备方法 (call_action)

用于执行设备提供的特殊功能,支持同时执行多个设备的方法。

请求示例:

{
  "userId": "小米账号ID",
  "serviceToken": "小米服务令牌",
  "deviceId": "通行证设备ID",
  "securityToken": "安全令牌",
  "action": "call_action",
  "params": [
    {
      "did": "设备ID1",
      "siid": 2,
      "aiid": 1,
      "in": []
    },
    {
      "did": "设备ID2",
      "siid": 3,
      "aiid": 2,
      "in": [{"piid": 1, "value": 50}]
    }
  ]
}

参数说明:

  • params: 方法调用参数数组,每项包含:
    • did: 设备ID
    • siid: 服务ID
    • aiid: 方法ID
    • in: 输入参数数组

参数说明

身份验证参数

  • userId: 小米账号ID,必填
  • serviceToken: 小米服务令牌,必填
  • deviceId: 通行证设备ID(PassportDeviceId),必填
  • securityToken: 安全令牌,必填

响应格式

所有操作的响应都是标准JSON格式,包含小米IoT平台的原始响应。

成功响应示例:

{
  "code": 0,
  "message": "ok",
  "result": [
    {
      "did": "设备ID1",
      "siid": 2,
      "piid": 1,
      "code": 0
    },
    {
      "did": "设备ID2",
      "siid": 3,
      "piid": 2,
      "code": 0
    }
  ]
}

错误处理

如果请求参数不完整或操作失败,Worker将返回错误信息:

{
  "code": 非零值,
  "message": "错误描述"
}

获取身份验证参数

要使用此Worker,您需要获取小米账号的身份验证参数:

  1. 在小米智能家居APP中登录
  2. 使用抓包工具获取请求中的userIdserviceTokendeviceIdsecurityToken

注意事项

  • 身份验证信息有时效性,过期后需要重新获取
  • 不要公开分享您的身份验证信息
  • 请确保Worker的URL使用HTTPS加密

设备参数查询

不同设备的siid、piid等参数不同,您可以参考以下方法获取:

  1. 访问 小米IoT设备规范网站 查询您的设备型号,该网站提供了详细的设备功能规范表,包含:

    • 各服务(service)的siid
    • 各属性(property)的piid及其可用值范围
    • 各方法(action)的aiid及参数要求
  2. 在该网站上搜索您的设备型号或浏览设备分类目录

  3. 查看设备规范中的服务(service)、属性(property)和方法(action)定义

  4. 根据规范文档确定您需要的操作对应的siid、piid、aiid参数

例如,对于智能灯泡,您可能会找到:

  • 电源控制服务(siid=2),包含开关属性(piid=1),可设置为true(开)或false(关)
  • 灯光控制服务(siid=3),包含亮度属性(piid=2),可设置为1-100的整数值

您也可以通过使用小米智能家居APP控制设备时抓包分析来获取这些参数。

优缺点分析

优点

  • 无需本地部署:无需在家庭网络中部署常驻服务,降低了硬件需求
  • 跨平台访问:可以从任何设备通过HTTP API调用控制智能设备
  • 简化集成:易于与其他自动化系统、网页或应用程序集成
  • 云端可靠性:Cloudflare的全球分布式网络提供高可用性和低延迟

缺点

  • 依赖云服务:完全依赖小米云服务和Cloudflare,当这些服务不可用时无法控制设备
  • 需要定期更新凭证:身份验证信息会定期过期,需要手动更新
  • 无法控制局域网设备:对于支持本地控制的设备,无法在互联网中断时使用本地控制功能
  • 受API限制:只能使用小米IoT平台公开的API功能,功能可能不如官方应用丰富

安全风险

凭证泄露风险

此Worker使用的身份验证信息(userId、serviceToken等)一旦泄露,攻击者可以完全控制您的所有智能设备。请确保:

  • 不要将Worker URL和身份验证信息公开分享
  • 定期更新身份验证信息
  • 考虑为Worker添加额外的访问控制(如API密钥)

重放攻击风险

尽管Worker实现了签名机制,但API请求仍可能面临重放攻击。攻击者可能通过以下方式利用:

  • 捕获有效的API请求并在短时间内重复发送
  • 利用nonce的时效性(本实现中nonce有60秒的有效期)

中间人攻击风险

如果您的网络通信被监听,可能面临以下风险:

  • 攻击者可以获取您的身份验证信息
  • 攻击者可以拦截并修改您的设备控制指令
  • 攻击者可以冒充服务器响应

为降低风险,请确保:

  • 仅通过HTTPS访问Worker
  • 在安全的网络环境中使用
  • 定期检查设备状态,确认控制指令是否按预期执行

权限过大风险

当前实现中,获取的身份验证信息通常具有完整的账户控制权限,可以控制账户下的所有设备。这可能导致:

  • 单点故障风险增加
  • 攻击影响范围扩大

建议在可能的情况下,使用有限权限的API访问令牌,而不是完整的账户访问凭证。

// 小米IoT平台设备控制 Cloudflare Worker
addEventListener('fetch', event => {
event.respondWith(handleRequest(event.request))
})
/**
* 处理HTTP请求
* @param {Request} request
*/
async function handleRequest(request) {
// 允许跨域请求
const corsHeaders = {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'POST, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type'
}
// 处理OPTIONS请求
if (request.method === 'OPTIONS') {
return new Response(null, { headers: corsHeaders })
}
// 只接受POST请求
if (request.method !== 'POST') {
return new Response('请使用POST方法', {
status: 405,
headers: { ...corsHeaders, 'Content-Type': 'text/plain; charset=UTF-8' }
})
}
try {
// 解析请求体
const requestBody = await request.json()
const {
userId,
serviceToken,
deviceId,
securityToken,
action,
params
} = requestBody
// 验证必要参数
if (!userId || !serviceToken || !deviceId || !securityToken) {
return new Response('缺少身份验证参数', {
status: 400,
headers: { ...corsHeaders, 'Content-Type': 'text/plain; charset=UTF-8' }
})
}
// 根据不同操作执行不同功能
if (action === 'set_properties') {
// 控制设备功能
if (!params || !Array.isArray(params) || params.length === 0) {
return new Response('缺少设备控制参数', {
status: 400,
headers: { ...corsHeaders, 'Content-Type': 'text/plain; charset=UTF-8' }
})
}
// 执行设备控制
const result = await setProperties(userId, serviceToken, deviceId, securityToken, params)
return new Response(JSON.stringify(result), {
headers: { ...corsHeaders, 'Content-Type': 'application/json' }
})
} else if (action === 'get_property') {
// 获取设备属性功能
if (!params || !Array.isArray(params) || params.length === 0) {
return new Response('缺少设备属性参数', {
status: 400,
headers: { ...corsHeaders, 'Content-Type': 'text/plain; charset=UTF-8' }
})
}
// 执行获取设备属性
const result = await getProperties(userId, serviceToken, deviceId, securityToken, params);
return new Response(JSON.stringify(result), {
headers: { ...corsHeaders, 'Content-Type': 'application/json' }
})
} else if (action === 'call_action') {
// 调用设备方法功能
if (!params || !Array.isArray(params) || params.length === 0) {
return new Response('缺少设备方法参数', {
status: 400,
headers: { ...corsHeaders, 'Content-Type': 'text/plain; charset=UTF-8' }
})
}
// 执行调用设备方法
const result = await callActions(userId, serviceToken, deviceId, securityToken, params);
return new Response(JSON.stringify(result), {
headers: { ...corsHeaders, 'Content-Type': 'application/json' }
})
} else {
return new Response('未知操作类型', {
status: 400,
headers: { ...corsHeaders, 'Content-Type': 'text/plain; charset=UTF-8' }
})
}
} catch (error) {
return new Response('处理请求时出错: ' + error.message, {
status: 500,
headers: { ...corsHeaders, 'Content-Type': 'text/plain; charset=UTF-8' }
})
}
}
/**
* 设置设备属性(支持多设备)
*/
async function setProperties(userId, serviceToken, deviceId, securityToken, params) {
// 设置属性URI
const uri = "/miotspec/prop/set";
// 构造参数
const data = {"params": params};
// 使用通用HTTP请求函数
return await httpRequest(userId, serviceToken, deviceId, uri, securityToken, data);
}
/**
* 获取设备属性(支持多设备)
*/
async function getProperties(userId, serviceToken, deviceId, securityToken, params) {
// 获取属性URI
const uri = "/miotspec/prop/get";
// 构造参数
const data = {"params": params};
// 使用通用HTTP请求函数
return await httpRequest(userId, serviceToken, deviceId, uri, securityToken, data);
}
/**
* 调用设备方法(支持多设备)
*/
async function callActions(userId, serviceToken, deviceId, securityToken, params) {
// 调用方法URI
const uri = "/miotspec/action";
// 构造参数
const data = {"params": params};
// 使用通用HTTP请求函数
return await httpRequest(userId, serviceToken, deviceId, uri, securityToken, data);
}
/**
* 通用HTTP请求函数
* @param {string} userId 用户ID
* @param {string} serviceToken 服务Token
* @param {string} deviceId 设备ID
* @param {string} uri 请求路径
* @param {string} securityToken 安全Token,POST请求需要
* @param {object} data 请求数据,有则POST请求,无则GET请求
*/
async function httpRequest(userId, serviceToken, deviceId, uri, securityToken = null, data = null) {
// 设置cookie
const cookie = `serviceToken=${serviceToken}; userId=${userId}; PassportDeviceId=${deviceId}`;
// 构造请求URL
const baseUrl = "https://api.io.mi.com/app";
const url = baseUrl + uri;
// 通用请求头
const headers = {
'User-Agent': 'APP/com.xiaomi.mihome APPV/6.0.103 iosPassportSDK/3.9.0 iOS/14.4 miHSTS',
'x-xiaomi-protocal-flag-cli': 'PROTOCAL-HTTP2',
'Cookie': cookie
};
if (data) {
// POST请求
if (!securityToken) {
throw new Error('POST请求需要提供securityToken');
}
// 签名数据
const nonce = generateNonce();
const signedNonce = await generateSignedNonce(securityToken, nonce);
const dataString = typeof data === 'string' ? data : JSON.stringify(data);
const signature = await generateSignature(uri, signedNonce, nonce, dataString);
// 构造请求参数
const params = {
_nonce: nonce,
data: dataString,
signature: signature
};
// 发送POST请求
const response = await fetch(url, {
method: 'POST',
headers: {
...headers,
'Content-Type': 'application/x-www-form-urlencoded'
},
body: new URLSearchParams(params)
});
// 解析响应
if (response.ok) {
return await response.json();
} else {
throw new Error(`请求失败: ${response.status} ${response.statusText}`);
}
} else {
// GET请求
const response = await fetch(url, {
method: 'GET',
headers: headers
});
// 解析响应
if (response.ok) {
return await response.json();
} else {
throw new Error(`请求失败: ${response.status} ${response.statusText}`);
}
}
}
/**
* 生成随机数
*/
function generateNonce() {
// 生成8字节随机数
const randomBytes = new Uint8Array(8)
crypto.getRandomValues(randomBytes)
// 获取当前时间,除以60并转为4字节
const timeBytes = new Uint8Array(4)
const time = Math.floor(Date.now() / 1000 / 60)
timeBytes[0] = (time >> 24) & 0xff
timeBytes[1] = (time >> 16) & 0xff
timeBytes[2] = (time >> 8) & 0xff
timeBytes[3] = time & 0xff
// 合并随机数和时间
const combined = new Uint8Array(12)
combined.set(randomBytes)
combined.set(timeBytes, 8)
// Base64编码
return btoa(String.fromCharCode.apply(null, combined))
}
/**
* 生成签名随机数
* @param {string} secret Base64编码的密钥
* @param {string} nonce Base64编码的随机数
*/
async function generateSignedNonce(secret, nonce) {
// 解码secret和nonce
const secretBytes = base64ToUint8Array(secret)
const nonceBytes = base64ToUint8Array(nonce)
// 合并secret和nonce
const combined = new Uint8Array(secretBytes.length + nonceBytes.length)
combined.set(secretBytes)
combined.set(nonceBytes, secretBytes.length)
// 使用SHA-256哈希
const hash = await crypto.subtle.digest('SHA-256', combined)
// Base64编码返回
return btoa(String.fromCharCode.apply(null, new Uint8Array(hash)))
}
/**
* 生成签名
* @param {string} url 接口路径
* @param {string} signedNonce 签名随机数
* @param {string} nonce 随机数
* @param {string} data 数据
*/
async function generateSignature(url, signedNonce, nonce, data) {
// 构造签名字符串
const signString = `${url}&${signedNonce}&${nonce}&data=${data}`
// 解码signedNonce为密钥
const keyBytes = base64ToUint8Array(signedNonce)
// 创建HMAC密钥
const key = await crypto.subtle.importKey(
'raw',
keyBytes,
{ name: 'HMAC', hash: { name: 'SHA-256' } },
false,
['sign']
)
// 计算HMAC
const signature = await crypto.subtle.sign(
'HMAC',
key,
new TextEncoder().encode(signString)
)
// Base64编码返回
return btoa(String.fromCharCode.apply(null, new Uint8Array(signature)))
}
/**
* Base64字符串转Uint8Array
* @param {string} base64 Base64编码的字符串
*/
function base64ToUint8Array(base64) {
const binary = atob(base64)
const bytes = new Uint8Array(binary.length)
for (let i = 0; i < binary.length; i++) {
bytes[i] = binary.charCodeAt(i)
}
return bytes
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment