时隔两年没写文章了,我胡汉三又回来了!
这段时间搬家, 买了一批小米智能家居,用起来感觉还不错。突然心血来潮,想试试看能不能通过API去操控米家的智能家居?如果能实现的话,那就能通过自己写程序去操控设备了,DIY等级直接提升了一个大档次!
想不如直接干,于是尝试去搜索这方面的资料。要实现控制,原理其实很简单,在米家APP能够远程控制设备,那么它的控制肯定是走的公网,只要把米家APP的操控协议抓包分析一下就知道了。网络协议无非就是http、socket、websocket这些,大概率还是http,当然最大的难点还是破解数据传输的加密算法。
在github上找了几天轮子后,很失望的发现类似这种现成的轮子没有,基本都是hass的插件,无法直接拿来调API。那么,只能借鉴一下插件,自己改造了。
流程分析
实际上发现米家还是通过HTTP协议去控制设备的,设备直接通过网络、蓝牙网关连接到小米云端,米家APP发送控制指令到云端,云端再下发控制指令到设备。
小米智能家居控制示意图
我们只需要抓米家APP的控制指令,就可以控制任意智能家居设备了。
模拟米家APP登录
米家的登录分三步:获取登录参数-账号登录–获取service token。
A. 获取登录参数
首先要获取登录参数,接口信息如下:
url = "https://account.xiaomi.com/pass/serviceLogin?sid=xiaomiio&_json=true" // 获取登录参数的URL
headers = {'User-Agent': 'APP/com.xiaomi.mihome APPV/6.0.103 iosPassportSDK/3.9.0 iOS/14.4 miHSTS'} // 使用IOS APP的UA
cookies = {'sdkVersion': '3.9', 'deviceId': device_id} // 随机生成16位字符串的设备ID
我们简单写几行代码,模拟这个过程:
url = "https://account.xiaomi.com/pass/serviceLogin?sid=xiaomiio&_json=true"
headers = {'User-Agent': 'APP/com.xiaomi.mihome APPV/6.0.103 iosPassportSDK/3.9.0 iOS/14.4 miHSTS'}
cookies = {'sdkVersion': '3.9', 'deviceId': device_id}
self.login_session.headers = headers
requests.utils.add_dict_to_cookiejar(self.login_session.cookies, cookies)
r = self.login_session.get(url)
result = json.loads(r.text[11:])
params = dict(qs=result.get("qs"), sid=result.get("sid"),
_sign=result.get("_sign"), callback=result.get("callback")
至此,我们获取到了需要的 qs、sid、_sign和callback 参数
B. 账号密码登录
接下来我们再构造一个请求,将上一步获取到的参数和账号密码放进去:
url = "https://account.xiaomi.com/pass/serviceLoginAuth2"
device_id = utils.get_random(16) if not self.token else self.token.get("deviceId")
params = self.__get_login_data(device_id)
params["_json"] = "true"
params["user"]=self.username
params["hash"]=utils.get_hash(self.password))
r = self.login_session.post(url, data=params)
result = json.loads(r.text[11:])
code = result.get("code")
if not code:
user_id = result.get("userId")
pass_token = result.get("passToken")
location = result.get("location")
nonce = result.get("nonce")
security_token = result.get("ssecurity")
通过返回的code判断一下,请求是不是成功了,如果成功了那么code就是0,获取到 userId、passToken、location、nonce、security_token这几个参数。
C. 获取service token
service token用于控制请求的鉴权,我们通过上面登录请求获取到的参数,再去获取一下service token。
n = f"nonce={str(nonce)}&{security_token}"
sign = utils.base64.b64encode(utils.hashlib.sha1(n.encode()).digest()).decode()
url = f"{location}&clientSign={parse.quote(sign)}"
r = self.login_session.get(url)
service_token = r.cookies.get("serviceToken")
至此,登录流程走完了。我们最终获取到了控制鉴权需要的service token和数据加密用的security token了。接下来就可以构造请求,控制设备了。因为 app的登录有效期一般比较长,所以可以把这个token本地保存一下,就不需要每次控制都去登录一次了。
构造控制设备的http请求
我们先用requests来构造一个“浏览器”,把http headers 和 cookies 添加上去,以后就能复用了。
self.session = requests.Session()
self.session.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'}
cookies = {
'serviceToken': service_token,
"userId": user_id,
"PassportDeviceId": device_id
}
requests.utils.add_dict_to_cookiejar(self.session.cookies, cookies)
另外,如果接口为POST类型,那么需要使用security token来对数据进行加密(签名)。先写一个加密算法:
def generate_nonce():
nonce = os.urandom(8) + int(time.time() / 60).to_bytes(4, 'big')
return base64.b64encode(nonce).decode()
def generate_signed_nonce(secret, nonce):
m = hashlib.sha256()
m.update(base64.b64decode(secret))
m.update(base64.b64decode(nonce))
return base64.b64encode(m.digest()).decode()
def generate_signature(url, signed_nonce, nonce, data):
sign = '&'.join([url, signed_nonce, nonce, 'data=' + data])
signature = hmac.new(key=base64.b64decode(signed_nonce),
msg=sign.encode(),
digestmod=hashlib.sha256).digest()
return base64.b64encode(signature).decode()
def sign_data(uri, data, secret):
if not isinstance(data, str):
data = json.dumps(data)
nonce = generate_nonce()
signed_nonce = generate_signed_nonce(secret, nonce)
signature = generate_signature(uri, signed_nonce, nonce, data)
return {'_nonce': nonce, 'data': data, 'signature': signature}
使用 sign_data 方法,传入请求的uri、请求数据和security token,对请求数据进行加密(签名)。
接下来写一个发送控制请求的公共方法:
# 构造公共请求
def __http_request(self, uri: str, data: dict = None) -> dict:
url = "https://api.io.mi.com/app" + uri
if data:
params = utils.sign_data(uri, data, self.security_token)
r = self.session.post(url, data=params)
else:
r = self.session.get(url)
return r.json()
这个方法封装了GET和POST请求,因为POST请求需要携带data参数,当判断未传入data时,则使用GET请求。
愉快地进行控制
好啦。接下来就是激动人心的时刻,我们将用代码来控制设备啦! 不过在此之前,我们还是要来了解一下小米的控制协议。
A. miot控制协议
米家的智能产品,有一套自己的控制协议:miot。所有的设备都遵循这套协议。
协议查询地址:https://home.miot-spec.com/
如我们搜索“小爱音箱pro”,在结果页中,点击下方连接就能看到控制协议了。
如下图,点进去后,可以看到一堆东西,siid,piid。刚开始看肯定感觉很凌乱,但理解了就很简单了。
归纳一下:
- siid:控制的大分类,比如2为声音控制,3为播放控制,4为麦克风控制。
- piid:控制的小分类,比如siid为2的大分类中,1为音量控制,2为是否静音。
- 属性(properties):属性分为读/写两种权限,比如音量有读和写属性,那么可以获取音箱的音量和设置音箱的音量。
- Actions(方法):其实这个方法类似于设置属性,也是用于控制设备的,它的参数为aiid。
- aiid:方法的分类。一般来说调用方法的话,可以省略掉piid。siid+aiid就可以了。
B. 用代码来控制
上一节我们构造了一个公共的请求方法, 那么我们可以直接拿这个方法来请求了。
因为控制设备的时候,我们不仅要siid、piid等参数,还需要一个did(设备ID)的参数,所以我们必须先获取一下账号内的设备列表,找到我们需要控制的设备ID。
# 获取设备ID
uri = "/home/device_list"
params = {"getVirtualModel": False, "getHuamiDevices": 0}
result = self.__http_request(uri, data=params)
device_list = result.get("result").get("list")
for device in self.device_list:
name = device.get("name") #通过名称查找设备ID 名称可通过米家APP自己改
if device_name in name:
did = device.get("did")
接下来我们就可以发送请求,来控制设备了,以设置小爱同学的音量为例:
# 设置属性uri:/miotspec/prop/set
# 获取属性uri:/miotspec/prop/get
# 使用方法uri:miotspec/action
uri = "/miotspec/prop/set"
params = dict(params=[{"did": did, "piid{
'code': 0, 'msg': 'success',
'data': {'did': '319671747', 'siid': 2, 'piid': 1, 'code': 0, 'exe_time': 0}
}": 2, "siid": 1, "value": 50}])
result = self.http_request(uri, params)
print(result)
# 返回值
{
'code': 0, 'msg': 'success',
'data': {'did': '319671747', 'siid': 2, 'piid': 1, 'code': 0, 'exe_time': 0}
}
然后就听见小爱音箱“dang”一下,音量设置成功!!
后记
折腾是不会止步的。接下来我会出后续文章,将整个控制方法封装一下,做到能够简单好用的调用。
当代码能够控制米家智能设备后,可玩性和可DIY性能直接上升了一个档次。再也不需要受到米家APP的束缚,真正做到“为所欲为”的控制。比如:
- 你可以通过对接QQ机器人、微信机器人,实现QQ、微信发消息控制智能家居;
- 微信、QQ消息通过小爱同学播报;
- 通过人脸识别、人体检测,搭配任意摄像机,实现回家欢迎、出门关灯等操作;
- 让灯光颜色根据你电脑屏幕颜色来自动变化;
- 家里门打开了自动启动XXX游戏;
- …………..
https://github.com/rytilahti/python-miio
谢谢楼主的分享,最近个人爱好研究米家的API,按照楼主的方式无法正常访问,希望可以得到楼主的帮忙
utils.get_hash 是什么函数,utils库中没有,是自义的吗?有没有源码
util是自己实现的 util 实用函数
相关代码放github了吗?
这方法应该失效了,抓了下包,发现 /miotspec/prop/set 的data参数并不是如文章里的那样的明文,并且header带有miot-encrypt-algorithm: ENCRYPT-RC4, miot-request-model: zhimi.airpurifier.ma4 等,这些并没有包括在博主的片段代码里,博主能否提供一下完整代码以供验证?
我用易语言重写了这个方法
这么给力 能分享看看补
尊敬的作者你好,我是第一次涉及这个领域,我需要调用米家 App 的监控 api 去获取某个特定时间点的截图,这里是我找到最接近的一篇教程,但是我并没有执行相关操作的经验,请问我应该如何执行以上代码,需要补充哪些方面的知识?(目前我仅学习了 c 加加的基本操作)
https://github.com/Do1e/mijia-api
亲测可用