原文链接: Yeelight 吸顶灯【凌动开关】怎么开:从抓包到接入 Home Assistant | Zain’s Blog
Yeelight 吸顶灯【凌动开关】怎么开:从抓包到接入 Home Assistant
1. 问题
两台 Yeelight 吸顶灯(皎月 480 升级版 yeelink.light.ceiling15)装了凌动开关面板,但:
- 米家 App 里找不到「凌动开关」选项
- 切到「默认样式 → 传统样式」也没有
- 只有 Yeelight 自己的 App 能开
灯本身是支持凌动的,问题只出在米家这边不显示入口。
于是有两个担心:
- 米家为什么不显示?是不是不支持?
- 万一 Yeelight App 哪天用不了,凌动还能不能开?
2. 先给结论
| 结论 | 说明 |
|---|---|
| 灯是支持的 | 皎月 480升级版(yeelink.light.ceiling15)支持凌动,更老的初代皎月(ceiling3 / ceiling4)不支持 |
| 米家不显示 ≠ 不支持 | 凌动是米家老 miIO 属性 smart_switch,不在 MiOT spec 里;米家插件没渲染这个入口,但属性本身存在且可写 |
写入方式是 set_ps |
set_ps ["cfg_smart_switch", "1"],1 开、0 关 |
| 走小米开放网关 | POST <``https://openapp.io.mi.com/openapp/device/rpc/``><did>,不是 Yeelight 自家云 |
| 可以完全脱离 App | 用 Home Assistant 里 xiaomi_home 的 token 就能调这个网关 |
3. 本次用的通道
| 通道 | 需要什么 | 重置后还能用 | 副作用 |
|---|---|---|---|
| 小米 openapp 云接口 | 任意已登录的小米系客户端 token | 能 | 无 |
4. 怎么抓到的
4.1 环境
- macOS + MuMu 模拟器(Android 12)
- 安装 Yeelight App,用米家账号登录
- Proxyman 做代理,模拟器 Wi-Fi 代理指向本机
9090
4.2 证书这步最容易卡住
Android 12 的「设置 → 安全」里如果只剩「安全状态 / SIM 卡锁定」,说明 ROM 把「加密与凭据」入口裁掉了,常规的「安装 CA 证书」根本走不到。
可行的做法是给模拟器 root,把证书塞进系统证书目录。但 /system 是只读的,mount -o rw,remount 会直接报 '/dev/block/sda1' is read-only,所以改用 bind mount:
# 1) 准备一份证书目录副本(放原始系统证书 + 自己的)
adb shell 'mkdir -p /data/local/tmp/cacerts && cp -a /system/etc/security/cacerts/. /data/local/tmp/cacerts/'
# 2) 证书按 subject_hash_old 命名,扩展名 .0,内容必须是 PEM
HASH=$(openssl x509 -in proxyman-ca.pem -subject_hash_old -noout)
adb push proxyman-ca.pem /data/local/tmp/cacerts/$HASH.0
adb shell "chmod 644 /data/local/tmp/cacerts/$HASH.0; chcon u:object_r:system_file:s0 /data/local/tmp/cacerts/$HASH.0"
# 3) 用 bind mount 覆盖系统证书目录
adb shell "mount --bind /data/local/tmp/cacerts /system/etc/security/cacerts"
# 4) 顺便把代理也设上(重启模拟器后 Wi-Fi 里的代理会丢)
adb shell settings put global http_proxy <Mac 局域网 IP>:9090
mount --bind 挂在重启后失效,需要重做;这也算一种安全的默认行为。
MuMu 开 root 可以用它的 CLI,不用点界面:
mumu-cli config 0 -s '{"vmRootEnable":true}'
mumu-cli restart 0
重启后 adb 端口可能变(16384 → 16385),要重新 adb connect。
4.3 抓到的请求
在 App 里把开关拨一次,Proxyman 里能看到两条几乎一样的请求:
POST <https://openapp.io.mi.com/openapp/device/rpc/123456789>
Content-Type: application/x-www-form-urlencoded
clientId=2882303761517308695
accessToken=V3_xxx
macKey=xxx&macAlgorithm=HmacSHA1&locale=zh-CN
data={"did":"123456789","id":1,"method":"set_ps","params":["cfg_smart_switch","1"]}
响应是 base64 的 JSON:
{"code":0,"message":"ok","result":["ok"]}
读状态用批量接口:
POST <https://openapp.io.mi.com/openapp/home/batchrpc>
data={"method":"get_prop","rpcs":[{"did":"123456789","params":["smart_switch"]}]}
属性顺序要和请求里的 params 对应,返回值是数组。
5. 用 Home Assistant 当客户端
5.1 关键发现
openapp.io.mi.com 不校验 macKey:随便填一个 27 位字符串也能通过,只认 clientId + accessToken。
而 xiaomi_home 集成本身就是小米官方客户端,它的 token 存在:
/config/.storage/xiaomi_home/miot_config/<uid>_cn.dict
文件是「JSON + 末尾 32 字节二进制」的混合格式,解析时要截断。HA 会自动续期(expires_in 约 43 分钟),所以每次运行现读即可,不硬编码。
5.2 脚本
保存为 /config/scripts/flex_switch.py:
#!/usr/bin/env python3
"""读写米家吸顶灯的凌动(smart_switch)开关。用法: flex_switch.py <did> get|on|off"""
import base64
import json
import sys
import urllib.parse
import urllib.request
from pathlib import Path
CONFIG_DIR = Path('/config/.storage/xiaomi_home/miot_config')
CLIENT_ID = '<xiaomi_home 的 clientId>'
ENDPOINT = '<https://openapp.io.mi.com/openapp>'
PROP = 'smart_switch'
def access_token():
for path in sorted(CONFIG_DIR.glob('*.dict')):
try:
raw = path.read_bytes().decode('utf-8', 'ignore')
data, _ = json.JSONDecoder().raw_decode(raw)
except Exception:
continue
token = (data.get('auth_info') or {}).get('access_token')
if token:
return token
raise RuntimeError('未找到 access_token')
def call(path, payload):
body = urllib.parse.urlencode({
'clientId': CLIENT_ID,
'accessToken': access_token(),
'macKey': '012345678901234567890123456', # 网关不校验,占位即可
'macAlgorithm': 'HmacSHA1',
'locale': 'zh-CN',
'data': json.dumps(payload, separators=(',', ':')),
}).encode()
req = urllib.request.Request(ENDPOINT + path, data=body, headers={
'User-Agent': 'mijia-sdk-ua',
'Content-Type': 'application/x-www-form-urlencoded',
})
text = urllib.request.urlopen(req, timeout=10).read().decode('utf-8', 'ignore')
try:
return json.loads(text)
except json.JSONDecodeError:
return json.loads(base64.b64decode(text + '=' * (-len(text) % 4)).decode())
def get_state(did):
res = call('/home/batchrpc', {'method': 'get_prop', 'rpcs': [{'did': did, 'params': [PROP]}]})
for item in res.get('result', []):
if item.get('did') == did and item.get('code') == 0:
return str(item['result'][0])
raise RuntimeError('读取失败: %s' % res)
def set_state(did, value):
res = call('/device/rpc/%s' % did, {
'did': did, 'id': 1, 'method': 'set_ps',
'params': ['cfg_' + PROP, value]})
if res.get('code') != 0:
raise RuntimeError('写入失败: %s' % res)
if __name__ == '__main__':
did, action = sys.argv[1], sys.argv[2]
if action == 'get':
print(get_state(did))
else:
set_state(did, '1' if action == 'on' else '0')
clientId 从 xiaomi_home/miot/const.py 的 OAUTH2_CLIENT_ID 取。
5.3 变成 HA 开关
configuration.yaml:
command_line:
- switch:
name: 主卧吸顶灯 凌动
unique_id: flex_switch_123456789
command_on: "python3 /config/scripts/flex_switch.py 123456789 on"
command_off: "python3 /config/scripts/flex_switch.py 123456789 off"
command_state: "python3 /config/scripts/flex_switch.py 123456789 get"
value_template: "{{ value | trim == '1' }}"
scan_interval: 1800
automations.yaml 加一条,解决 HA 启动后状态不准:
- id: 'flex_switch_refresh_on_start'
alias: 启动后刷新凌动开关
trigger:
- platform: homeassistant
event: start
action:
- service: homeassistant.update_entity
entity_id:
- switch.zhu_wo_xi_ding_deng_ling_dong
- switch.ci_wo_xi_ding_deng_ling_dong
mode: single
6. 踩过的坑
| 坑 | 现象 | 处理 |
|---|---|---|
| Android 12 无「加密与凭据」 | 设置里只有安全状态、SIM 卡锁定 | root + bind mount 系统证书目录 |
/system 无法 remount |
'/dev/block/sda1' is read-only |
不要硬改,用 mount --bind |
| 代理设置重启丢失 | settings get global http_proxy为 null |
用 settings put global 重设 |
| 模拟器 adb 端口变化 | device not found |
重启后看 lsof 或重新 adb connect |
value_template 永远不成立 |
实体一直显示 off | 脚本输出带换行,用 `{{ value |
| 重启后状态是 off | 要等一个轮询周期才准 | 加 homeassistant start 触发 update_entity 的自动化 |
7. 边界与风险
- token 敏感:
accessToken等价于账号在小米云上的操作凭证;截图、抓包文件(HAR)用完即删,不要贴进文章或仓库。 - 云接口是叠加不是替换:
openapp是米家 App 自己在用的通道,短期不会下线;但 Yeelight App/模拟器仍建议保留一份做兜底。 - 设备重置会做什么:
smart_switch回默认。只要设备重新绑回同一个米家账号,云接口方案照常可用,不需要重装 App。 - 不适用于:没有实现这个属性的灯具(比如只走蓝牙 Mesh 的筒灯),本方案无效。
8. 一句话总结
「米家看不到凌动开关」不是灯不支持,而是新版插件不渲染 miIO 老属性;真正的开关是 smart_switch,用 set_ps 写。拿到任意小米系客户端的 token 就能远程设它——包括 Home Assistant 自己的 xiaomi_home,于是这件事就从「必须依赖某个旧 App」变成了「HA 里的开关」。