智能家居控制方案详解:如何用AI远程控制米家灯
如何用Python控制米家智能设备 - 完整指南
背景
米家(小米智能家居)是国内最大的IoT平台之一,支持数千种智能设备。很多用户希望能通过编程方式控制这些设备,而不仅限于使用米家App。
本文是作者实际打通米家设备控制的完整记录,包括技术方案、踩坑总结、安全分析等。无论你是想学习IoT控制,还是想自己搭建智能家居系统,本文都能提供有价值的参考。
一、米家设备控制的技术原理
1.1 MiIO vs MIoT 协议
通过搜索发现,米家设备主要使用两种协议:
| 协议 | 名称 | 设备类型 | 特点 |
|---|---|---|---|
| miIO | 传统协议 | 旧设备 | 需要手动探索控制方式 |
| MIoT | 现代协议 | 新设备 | 自动获取设备规格(更先进) |
关键发现:python-miio官方文档明确指出:
"Most modern (MIoT) devices are automatically supported by the generic miot integration. Internally, it uses miot spec files to find out about supported features..."
翻译:大多数现代(MIoT)设备被generic miot集成自动支持。内部使用miot spec文件来获取支持的功能。
1.2 miot-spec.com 设备规格网站
通过搜索发现有一个关键网站:home.miot-spec.com(米家设备规格数据库)
这是米家设备的"身份证",记录了各种设备的siid/aiid/piid对应关系。
1.3 设备模型:服务-属性-操作
MiIO/MIoT协议的核心是服务-属性-操作模型:
设备 (Device)
├── 服务1 (siid=1: 设备信息)
│ ├── 属性 (piid=1: 设备名称)
│ └── 属性 (piid=2: 固件版本)
├── 服务2 (siid=2: 电源)
│ ├── 属性 (piid=1: 开关状态: on/off)
│ ├── 操作 (aiid=1: 打开)
│ └── 操作 (aiid=2: 关闭)
├── 服务3 (siid=3: 灯光)
│ ├── 属性 (piid=1: 亮度: 0-100)
│ └── 操作 (aiid=1: 设置亮度)
└── ...- siid (Service ID):服务ID,设备的功能模块(电源、灯光、马达等)
- piid (Property ID):属性ID,服务的属性(状态、数值等)
- aiid (Action ID):操作ID,服务的可执行动作(开、关、开始、停止等)
1.4 厂商实现差异(核心问题)
这是打通设备控制的核心问题:不同厂商对协议的实现差异巨大。
| 厂商 | 设备类型 | siid=2的定义 | aiid=1 | params | aiid=3 |
|---|---|---|---|---|---|
| 石头科技 | 扫地机器人 | 电源服务 | 开始清扫 | [] | 暂停 |
| Yeelight | 智能灯 | 电源服务 | 打开 | ["on"] | 关闭 |
| 云米 | 空气净化器 | 电源服务 | 打开 | ["on"] | 关闭 |
| 追觅 | 吸尘器 | 电源服务 | 打开 | [1] | 关闭 |
可以看到:即使是同一个"开"功能:
- 石头科技:用 aiid=1,params=[](空数组)
- Yeelight:用 aiid=1,params=["on"](字符串)
- 追觅:用 aiid=1,params=[1](数字)
这正是用API控制的难点所在。
二、调研过程
2.1 搜索方法
本次调研使用了以下搜索渠道:
- GitHub搜索:搜索Python库的stars数和功能介绍
- 官方文档分析:分析python-miio官方文档(python-miio.readthedocs.io)
- 设备规格网站:发现miot-spec.com设备规格数据库
- 实际测试:在真实设备上测试控制命令
2.2 搜索结果
| 库/资源 | 来源 | 关键信息 |
|---|---|---|
| python-miio | GitHub (4189 stars) | 支持MIoT自动获取设备规格 |
| mijia-api | GitHub (506 stars) | 封装了登录和设备操作 |
| miot-spec.com | 网站 | 设备规格数据库 |
2.3 python-miio的设备发现机制
通过搜索python-miio官方文档,发现其设备发现机制:
- mDNS发现:通过mDNS自动发现局域网设备,获取设备类型信息
- Handshake发现:通过握手获取设备token,但无法获取设备类型
- 云端获取:通过micloud从云端获取所有设备的token
官方文档说明:
"The miiocli tool can fetch the tokens from the cloud if you have micloud package installed.
Executing the command will prompt for the username and password, as well as the server locale to use for fetching the tokens."
2.4 两种方案的详细对比
| 特性 | mijia-api | python-miio |
|---|---|---|
| 协议支持 | MiIO | MiIO + MIoT |
| 设备发现 | 云端获取 | 局域网发现 + 云端获取 |
| 控制参数 | 需手动探索 | 自动获取 |
| 学习曲线 | 简单 | 较陡 |
| 维护状态 | 个人维护 | 社区活跃 |
| 适用场景 | 快速上手 | 深度定制 |
建议:
- 新手入门:选mijia-api
- 需要支持大量设备:选python-miio
三、技术实现
3.1 环境准备
# 安装依赖
pip install mijiaAPI qrcode pillow3.2 扫码登录(mijia-api方式)
重要:login()会阻塞120秒,需要保持进程活着!
from mijiaAPI import mijiaAPI
api = mijiaAPI()
# login()会阻塞120秒,等待扫码
# 需要保持进程活着,扫码后服务器轮询确认
result = api.login()
if result:
print("登录成功!")
# result包含: serviceToken, passToken, userId, expireTime
print(f"用户ID: {result.get('userId')}")
print(f"过期时间: {result.get('expireTime')}")
else:
print("登录失败")3.3 获取设备列表
devices = api.get_devices_list()
for d in devices:
print(f"设备名称: {d.get('name')}")
print(f"设备ID: {d.get('did')}") # 控制用
print(f"设备型号: {d.get('model')}") # 查询规格用
print(f"在线状态: {d.get('online')}")
print("---")3.4 控制设备
# 格式说明
action = {
"did": "设备ID", # 设备的唯一标识(从设备列表获取)
"siid": 服务ID, # 功能模块(2=电源,3=灯光等)
"aiid": 操作ID, # 具体操作(1=开,2=关等)
"params": [参数] # 操作参数
}
result = api.run_action(action)
# 返回格式
# 成功: {"code": 0, "message": "成功", "exe_time": 10}
# 失败: {"code": -704040005, "message": "Action不存在"}3.5 获取设备属性
# 获取设备属性
props = api.get_devices_prop([
{"did": "设备ID", "siid": 2, "piid": 1}
])
# 返回: {"did": "xxx", "siid": 2, "piid": 1, "value": "on", "code": 0}3.6 设置设备属性
# 设置设备属性
props = api.set_devices_prop([
{"did": "设备ID", "props": {"power": "on"}}
])3.7 完整示例:获取所有设备并分类
from mijiaAPI import mijiaAPI
api = mijiaAPI()
api.login()
devices = api.get_devices_list()
# 按类型分类
lights = []
vacuums = []
others = []
for d in devices:
model = d.get('model', '')
if 'light' in model or '灯' in d.get('name', ''):
lights.append(d)
elif 'vacuum' in model or '扫地' in d.get('name', ''):
vacuums.append(d)
else:
others.append(d)
print(f"灯: {len(lights)}个")
for l in lights:
print(f" - {l.get('name')}: {l.get('did')}")
print(f"扫地机器人: {len(vacuums)}个")
for v in vacuums:
print(f" - {v.get('name')}: {v.get('did')}")
print(f"其他设备: {len(others)}个")四、封装成REST API服务
4.1 完整示例
from flask import Flask, request, jsonify
from mijiaAPI import mijiaAPI
import threading
import qrcode
app = Flask(__name__)
mi_api = None
def generate_qr(url):
"""生成二维码"""
qr = qrcode.QRCode(version=1, box_size=10, border=5)
qr.add_data(url)
qr.make(fit=True)
img = qr.make_image(fill_color="black", back_color="white")
img.save("static/mijia-login-qr.png")
def do_login():
"""后台登录"""
global mi_api
mi_api = mijiaAPI()
try:
result = mi_api.login()
print(f"登录结果: {result is not None}")
except Exception as e:
print(f"登录错误: {e}")
@app.route("/login", methods=["POST"])
def login():
"""触发扫码登录"""
global mi_api
if mi_api is not None:
return jsonify({"message": "已登录"})
# 启动后台登录线程
login_thread = threading.Thread(target=do_login)
login_thread.start()
return jsonify({
"message": "请在120秒内用米家APP扫码",
"qr_url": "/static/mijia-login-qr.png"
})
@app.route("/devices")
def devices():
"""获取设备列表"""
if not mi_api:
return jsonify({"error": "请先登录"}), 401
return jsonify(mi_api.get_devices_list())
@app.route("/control", methods=["POST"])
def control():
"""控制设备"""
if not mi_api:
return jsonify({"error": "请先登录"}), 401
data = request.json
result = mi_api.run_action({
"did": data["did"],
"siid": data["siid"],
"aiid": data["aiid"],
"params": data.get("params", [])
})
return jsonify(result)
@app.route("/status")
def status():
"""检查登录状态"""
return jsonify({"logged_in": mi_api is not None})
if __name__ == "__main__":
app.run(host="0.0.0.0", port=8080)4.2 客户端调用示例
# 登录
curl -X POST http://localhost:8080/login
# 获取设备
curl http://localhost:8080/devices
# 开灯
curl -X POST http://localhost:8080/control
-H "Content-Type: application/json"
-d '{"did": "123456", "siid": 2, "aiid": 1, "params": ["on"]}'
# 关灯
curl -X POST http://localhost:8080/control
-H "Content-Type: application/json"
-d '{"did": "123456", "siid": 2, "aiid": 1, "params": ["off"]}'五、踩坑记录与解决方案
5.1 常见错误代码
| 错误代码 | 含义 | 原因 | 解决 |
|---|---|---|---|
| -704040005 | Action不存在 | siid或aiid错误 | 尝试其他组合 |
| -705201023 | Property不可写 | 属性只读 | 使用action而非property |
| -8 | data type not valid | params参数类型错误 | 检查params格式:字符串/数字/数组 |
| -4008 | 设备不在线 | 设备离线 | 检查设备网络 |
| -401 | 未登录 | token失效 | 重新扫码登录 |
5.2 探索设备命令的方法
对于mijia-api这种库,需要手动探索siid/aiid组合:
def explore_device(api, device_id):
"""探索设备的控制命令"""
results = []
print(f"开始探索设备: {device_id}")
for siid in range(1, 20):
for aiid in range(1, 20):
# 尝试不同的参数
params_list = [
[], # 空数组
["on"], # 字符串开
["off"], # 字符串关
[1], # 数字开
[0], # 数字关
["toggle"], # 切换
]
for params in params_list:
try:
result = api.run_action({
"did": device_id,
"siid": siid,
"aiid": aiid,
"params": params
})
if result.get("code") == 0:
finding = {
"siid": siid,
"aiid": aiid,
"params": params,
}
results.append(finding)
print(f"✅ 成功! {finding}")
except Exception as e:
pass
print(f"探索完成,找到 {len(results)} 个有效命令")
return results5.3 已验证的命令
| 设备 | siid | aiid | params | 功能 |
|---|---|---|---|---|
| 落地灯 | 2 | 1 | ["on"] | 开 |
| 落地灯 | 2 | 1 | ["off"] | 关 |
| 扫地机器人 | 2 | 1 | [] | 开始清扫 |
| 扫地机器人 | 2 | 2 | [] | 回充 |
| 扫地机器人 | 2 | 3 | [] | 停止/暂停 |
六、问题分析:为什么用API控制这么复杂?
6.1 米家APP帮用户做了什么
用户用米家App控制设备时,不需要关心siid/aiid/params,因为APP已经封装好了。APP内部维护了每个设备的控制参数映射表,用户只需要点击"开/关"按钮。
6.2 用API控制的问题
用Python API控制时:
- 没有封装:需要自己处理协议细节
- 文档缺失:厂商不会公开siid/aiid的对应关系
- 每个设备不同:即使是同一功能,控制参数也不同
- 没有统一标准:各厂商按自己的理解实现
这解释了为什么用Python API控制比用App复杂很多倍——因为App帮用户屏蔽了所有这些复杂性。
6.3 更好的方案选择
通过搜索发现,python-miio 提供了更先进的方案:
- 自动获取设备规格:MIoT设备自动从miot-spec.com下载规格
- 命令行工具:miiocli可以直接控制设备
- 无需手动探索:库自动处理siid/aiid
- 设备发现:自动发现局域网内的设备
七、安全分析
7.1 token机制
扫码登录后获取的token包含:
- serviceToken:API调用凭证,有效期数月到数年
- passToken:用户验证凭证
- userId:用户标识
7.2 风险评估
理论风险:
- 拿到token可以调用API控制设备
- 可以获取设备列表
- 可以执行已创建的场景
实际情况:
- token有有效期(较长)
- 可能与服务IP绑定(社区讨论多,缺乏权威资料)
- 官方可以强制下线
7.3 防护建议
- 不暴露token:不要提交到GitHub或发到群里
- 定期检查:米家App → 设置 → 账号与安全 → 登录设备
- 异常处理:发现异常立即修改密码并重新登录
- 最小权限:测试用小号,不要用主要账号
7.4 紧急处理
如果怀疑token泄露:
- 修改小米账号密码
- 米家App退出所有设备登录
- 重新扫码获取新token
八、实际应用场景
8.1 定时任务
import schedule
import time
from mijiaAPI import mijiaAPI
def morning_routine():
"""早间routine"""
api = mijiaAPI()
api.login()
# 开灯
api.run_action({"did": "灯ID", "siid": 2, "aiid": 1, "params": ["on"]})
def night_routine():
"""晚间routine"""
api = mijiaAPI()
api.login()
# 关灯
api.run_action({"did": "灯ID", "siid": 2, "aiid": 1, "params": ["off"]})
# 扫地机器人回充
api.run_action({"did": "机器人ID", "siid": 2, "aiid": 2, "params": []})
# 设置定时
schedule.every().day.at("07:00").do(morning_routine)
schedule.every().day.at("22:00").do(night_routine)
while True:
schedule.run_pending()
time.sleep(60)8.2 命令行工具
#!/usr/bin/env python3
import sys
from mijiaAPI import mijiaAPI
COMMANDS = {
"on": {"siid": 2, "aiid": 1, "params": ["on"]},
"off": {"siid": 2, "aiid": 1, "params": ["off"]},
"start": {"siid": 2, "aiid": 1, "params": []},
"stop": {"siid": 2, "aiid": 3, "params": []},
"return": {"siid": 2, "aiid": 2, "params": []},
}
def main():
if len(sys.argv) < 3:
print("用法: miot <设备ID> <命令>")
print(f"可用命令: {list(COMMANDS.keys())}")
sys.exit(1)
did = sys.argv[1]
cmd = sys.argv[2].lower()
if cmd not in COMMANDS:
print(f"未知命令: {cmd}")
sys.exit(1)
api = mijiaAPI()
action = COMMANDS[cmd].copy()
action["did"] = did
result = api.run_action(action)
if result.get("code") == 0:
print("成功!")
else:
print(f"失败: {result.get('message')}")
if __name__ == "__main__":
main()使用:
python miot.py 123456789 on # 开灯
python miot.py 123456789 off # 关灯
python miot.py 123456789 start # 开始清扫九、总结
9.1 核心要点
- 协议层面:MiIO(传统)和MIoT(现代)两种协议,后者支持自动获取设备规格
方案层面:
- mijia-api:需要手动探索siid/aiid,上手简单
- python-miio:自动获取,更先进但学习曲线较陡
- 搜索发现:miot-spec.com是关键的设备规格数据库
- 核心问题:厂商实现差异大,没有统一标准,导致控制参数需要逐一探索
- 安全层面:token机制存在风险,建议做好防护
- 应用层面:可封装REST API、命令行工具,定时任务等多种形式
9.2 下一步建议
- 尝试python-miio方案,体验自动获取设备规格
- 探索更多设备的控制命令,丰富命令库
- 搭建完整的智能家居控制系统
参考资料
- GitHub - python-miio (4189 stars)
- GitHub - mijia-api (506 stars)
- python-miio官方文档 (python-miio.readthedocs.io)
- miot-spec.com 设备规格网站