热爱技术
专注分享

小米智能家居第三方控制之一:提取米家API并控制

时隔两年没写文章了,我胡汉三又回来了!

这段时间搬家, 买了一批小米智能家居,用起来感觉还不错。突然心血来潮,想试试看能不能通过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。刚开始看肯定感觉很凌乱,但理解了就很简单了。

归纳一下:

  1.  siid:控制的大分类,比如2为声音控制,3为播放控制,4为麦克风控制。 
  2. piid:控制的小分类,比如siid为2的大分类中,1为音量控制,2为是否静音。
  3. 属性(properties):属性分为读/写两种权限,比如音量有读和写属性,那么可以获取音箱的音量和设置音箱的音量。
  4. Actions(方法):其实这个方法类似于设置属性,也是用于控制设备的,它的参数为aiid。
  5. 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的束缚,真正做到“为所欲为”的控制。比如:

  1. 你可以通过对接QQ机器人、微信机器人,实现QQ、微信发消息控制智能家居;
  2. 微信、QQ消息通过小爱同学播报;
  3. 通过人脸识别、人体检测,搭配任意摄像机,实现回家欢迎、出门关灯等操作;
  4. 让灯光颜色根据你电脑屏幕颜色来自动变化;
  5. 家里门打开了自动启动XXX游戏;
  6. …………..
赞(86) 打赏
未经允许不得转载:小伟博客 » 小米智能家居第三方控制之一:提取米家API并控制

评论 11

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址
  1. #1
  2. #2

    谢谢楼主的分享,最近个人爱好研究米家的API,按照楼主的方式无法正常访问,希望可以得到楼主的帮忙

    Hmantic2年前 (2022-05-05)回复
  3. #3

    utils.get_hash 是什么函数,utils库中没有,是自义的吗?有没有源码

    微风2年前 (2022-06-30)回复
    • util是自己实现的 util 实用函数

      nnnnnnnnn6个月前 (10-14)回复
  4. #4

    相关代码放github了吗?

    CS QGB2年前 (2022-08-01)回复
  5. #5

    这方法应该失效了,抓了下包,发现 /miotspec/prop/set 的data参数并不是如文章里的那样的明文,并且header带有miot-encrypt-algorithm: ENCRYPT-RC4, miot-request-model: zhimi.airpurifier.ma4 等,这些并没有包括在博主的片段代码里,博主能否提供一下完整代码以供验证?

    dzco2年前 (2022-08-27)回复
  6. #6

    我用易语言重写了这个方法

    杨杰1年前 (2022-11-18)回复
    • 这么给力 能分享看看补

      ddw11个月前 (05-24)回复
  7. #7

    尊敬的作者你好,我是第一次涉及这个领域,我需要调用米家 App 的监控 api 去获取某个特定时间点的截图,这里是我找到最接近的一篇教程,但是我并没有执行相关操作的经验,请问我应该如何执行以上代码,需要补充哪些方面的知识?(目前我仅学习了 c 加加的基本操作)

    youli1个月前 (03-21)回复
  8. #8

觉得文章有用就打赏一下文章作者

非常感谢你的打赏,我们将继续给力更多优质内容,让我们一起创建更加美好的网络世界!

支付宝扫一扫打赏

微信扫一扫打赏