我把家里的格兰斯柯新风"偷"进了 Home Assistant——一篇人人可复刻的小程序逆向实录

image-20261106175539848

家里装的格兰斯柯新风一体机,控制全靠微信:小程序叫"格兰斯柯智能控制",服务号里叫"格兰斯柯新风系统"。
设备不错,但我想要的从来不只是"打开微信→找到小程序→点开关"——
我想要它在 Home Assistant 里:跟传感器联动、进自动化、上仪表盘、接入小爱同学一句话开关。

于是花了一晚上,把它的通信协议逆向了出来。整个过程不碰设备一个字节,全程只分析"微信和云端自己发的数据"。
这篇文章把每一步都摊开讲——照着做,你也能把自己的任何"微信小程序家电"接进 Home Assistant。
语音联调翻了三次车;接入当晚连接就断了、第二天实体全灰——这种"不到 24 小时就熄火"的隐患,也一并拆解。


先看成果

集成完成后,Home Assistant 里多出了 21 个实体:

类型实体
风机开关、1~3 档风速(低/中/高一一对应)、智能/手动模式
传感器 ×9PM2.5、CO₂、TVOC、室内外温湿度、室外 PM2.5、滤芯寿命 96%
能耗 ×3累计功耗 103.42 度 / 今日 16.52 度 / 昨日 40.54 度(与微信"功耗查询"同源)
开关 ×4辅热、童锁、新风(外循环)、屏幕显示
定时 ×2定时开机 / 定时关机(微信"更多"页同款功能,可设每天/单次/指定星期)
空调模块制热/制冷/除湿/送风 + 温度设定
锁童锁

也就是说,服务号"更多"页里的东西,一个不落全部进了 HA:

服务号更多页
▲ 微信服务号里的"更多"页——功耗查询、定时开/关机、电辅热、童锁,这些在 HA 里都有对应实体

风机控制卡片
▲ HA 里的风机控制:拖动滑条直接控制真机,低/中/高与 1/2/3 档一一对应

CO2 实时曲线
▲ CO₂ 传感器 + 历史曲线,和微信小程序看到的是同一份数据

而且——微信小程序照样能用。两条控制通路互不干扰:家里人在微信里开,我在 HA 里关,谁也不耽误谁。


零、先讲原理(看懂了这个,后面全是体力活)

先别急着敲命令。我们要搞清楚一件事:微信小程序到底是怎么控制你的空调的?

它当然不是通过魔法。你的手机链路是这样的:

微信小程序 ──HTTPS──> 厂商服务器(iot.szglancetech.com)
                           │
                           ▼
                    机智云 Gizwits 物联网平台(设备真正挂的地方)
                           ▲
                           │ WebSocket 长连接
                      你家的新风机

关键洞察有三个:

洞察 1:绝大部分白电的"智能",都不是厂商自研的。
它们接的是第三方物联网云平台——机智云(Gizwits)、涂鸦(Tuya)、苏宁小 Biu、海尔优家……本案的格兰斯柯接的是机智云。而机智云是有公开 OpenAPI 文档的,协议是标准化的。这意味着:只要拿到四个参数(appid、uid、token、设备 did),我们就能用官方接口直接控制设备——根本不需要破解任何东西。

洞察 2:小程序包在电脑上是加密落盘的,但运行时数据在内存里是明文的。
Linux/Windows 版微信会把小程序包(.wxapkg)缓存在本地,但它是加密的(V1MMWX 头)。新手到这里往往就卡死了——别硬刚加密,让小程序自己运行起来,然后去进程内存里捞它正在使用的登录凭证和报文样例。这是整个流程里最省力的一步妙棋。

洞察 3:服务号里的"H5 控制页"就是一张普通网页,它的源码公开可下载。
"格兰斯柯新风系统"服务号里的控制页面,本质是 http://iot.szglancetech.com/gizwits-wxjw/ 这张 Vue 网页。虽然它要求在微信里打开,但 JS 源码谁都能下载——所有控制指令、加密算法、数据点定义,全明晃晃写在里面。

H5 微信墙
▲ 离开微信打开是"请在微信客户端打开链接"——但源码是公开的

一句话总结我们的策略:

内存拿凭证 → 源码学协议 → 官方 API 做集成


一、准备工作

你需要:

  • ✅ 一台装了电脑版微信的机器(Windows / macOS / Linux 都行,本文以 Linux 为例,其他系统只是路径不同)
  • ✅ 在电脑微信里打开目标小程序(重要!后面会讲为什么)
  • ✅ 一台跑着 Home Assistant 的机器(Docker 安装的最常见)
  • ✅ 会复制粘贴,会重启容器,仅此而已

第 1 步:打开小程序,找到它的"身份证号"

先在电脑微信里打开"格兰斯柯智能控制"小程序,随便点点设备页。然后开个终端,看看微信进程的启动参数:

ps aux | grep WeChatAppEx | grep -oE "wmpf-appid=wx[0-9a-f]+"

找到小程序包

输出里的 wxb84aee9a584036c7 就是这个小程序的 AppID——相当于它的身份证号,后面到处会用到。

顺手还能找到它的本地缓存包:

# Linux 版微信的小程序包路径
find ~/.xwechat/radium -path "*packages*" -name "*.wxapkg"

小程序图标
▲ 这个 140×140 的小图标,就是从微信缓存里翻出来的小程序本体之一

打开这个包看看?文件头是 V1MMWX——微信对小程序包做了加密,前 1024 字节 AES-256-CBC,密钥从你的微信账号派生。

新手别在这里死磕解密。 我们直接跳到更聪明的路:内存取证。

二、内存取证:让小程序"自己交出"登录凭证

小程序正在运行,意味着它刚通过 HTTPS 拿到了登录 token、刚连上了机智云的 WebSocket、刚收到设备状态——这些数据此刻就躺在微信进程的内存里,明文。

我们写个小脚本,扫描微信进程的可读内存段,找三类东西:

  1. login_req 开头的 WebSocket 登录报文(金矿:里面有现成的 appid + uid + token)
  2. "did" 设备编号
  3. 所有非腾讯系的 URL(找到厂商服务器地址)

核心代码其实非常短(完整版在文末的 gizwits_hunter.py,一键运行):

import re

WS_LOGIN_RE = re.compile(
    rb'\{"cmd":\s*"login_req",\s*"data":\s*\{[^{}]*?"appid":\s*"([0-9a-f]{32})"'
    rb'[^{}]*?"uid":\s*"([0-9a-f]{32})"[^{}]*?"token":\s*"([A-Za-z0-9._\-]{16,64})"'
)

for pid in 找到的微信进程:
    with open(f"/proc/{pid}/mem", "rb") as mem:
        for 起始, 结束 in 可读内存段:
            buf = mem.read(...)
            for m in WS_LOGIN_RE.finditer(buf):
                print("抓到了!", m.groups())

⚠️ 需要管理员权限(读别人进程的内存当然要 root),并且小程序必须开着(token 才没被回收):

sudo python3 gizwits_hunter.py --phone-id my-ha-001

一跑,惊喜来了:

内存扫描结果

我们直接从内存里挖出了一整条可用的 WebSocket 登录报文:

{ "cmd": "login_req", "data": {
    "appid":  "8615f64ed9f249b69628da35136f5271",
    "uid":    "f228c2be692547bea83fc5674deb2a0d",
    "token":  "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9...",
    "p0_type": "attrs_v4", "heartbeat_interval": 180 } }

同时还捞到了一堆关键情报:

  • 机智云官方通道:wss://m2m.gizwits.com:8880/ws/app/v1
  • 厂商服务器:http://iot.szglancetech.com/gizwits-wxjw/
  • 设备编号:did = DT0LhnOwjSC7om2Yyb23vr

gizwits_hunter.py 的完整输出长这样(注册自管账号、绑定设备、生成 HA 配置,一气呵成):

一键脚本输出

到这里,这场逆向已经基本结束了——剩下的就是用官方协议做集成。

三、读源码:把控制指令"翻译"出来

有凭证还不够,我们得知道发什么字段、什么值。这就要请出服务号里那张 H5 网页的源码了。

把 app.js(951KB)下载下来,用 grep 顺藤摸瓜。先看它怎么登录厂商服务器:

还原 JWT 算法

// H5 源码里的密码函数 d(t):
function d(t) {
  var e = md5(t);
  if (32 === e.length) {
    var i = e.toLowerCase().substr(8, 16);   // ← 就这?
    return console.log("jwt user pw", t, i), i
  }
}

密码 = md5(你的openid) 的第 8~24 位。 一个 md5 截断,就拿到了厂商服务器的通行证。用 curl 验证一下,果然拿到了正式的 JWT token。

接着挖控制指令。H5 里每个按钮的点击处理都明明白白:

界面按钮实际下发的字段
开/关机{"power": true/false}
风速 1~3 档{"speed": 1~3}(注意:只发这一个字段,别捎带别的——原因见第九节踩坑实录)
智能/手动{"auto": true/false}
新风(外循环){"fresh": true/false}(false = 内循环)
辅热{"heat": true/false}
童锁{"childlock": true/false}
空调模块 制热/制冷/除湿/送风{"acmode": 1/2/3/4}
温度调节{"actempcool": 16~31}

功耗和定时开关机藏得深一点,但也在源码里现出原形:

  • 功耗数据点是 energyA(累计)/ energyT(今日)/ energyY(昨日),换算就一句 (v/100).toFixed(2) 度;
  • 定时开关机是一个 32 位位域整数:启用<<24 | 时<<16 | 分<<8 | 星期重复,重复位 0~5 是周一到周六、位 6 是周日——H5 里 transformAlarmdata 和 calcAlarm 两个函数互为镜像,一个编码一个解码。

传感器的数据点名字也一并拿到了:pm25、co2、tvoc、tempin、humidity、surplus1(滤芯剩余寿命)……

四、走正门:注册自己的机智云账号,"合法地"接管设备

现在到了整篇文章最漂亮的转折。

内存里抓到的 token 是微信账号的,63 天后过期,而且"借"别人的账号总归不优雅。但机智云是正规物联网平台,它有匿名注册接口——任何人都可以创建一个自己的账号:

curl -X POST https://api.gizwits.com/app/users \
  -H 'X-Gizwits-Application-Id: 8615f64ed9f249b69628da35136f5271' \
  -d '{"phone_id":"my-home-assistant","lang":"zh-cn"}'
{ "token": "xxx...", "uid": "1854ffdcb16649ebb5b6892f8c6671c3",
  "expire_at": 1809243696 }

再用从 /app/bindings 接口查到的设备 did + passcode,把设备绑定到我们自己的账号:

curl -X POST https://api.gizwits.com/app/bindings \
  -H 'X-Gizwits-User-Token: <刚拿到的token>' \
  -d '{"devices":[{"did":"DT0LhnOwjSC7om2Yyb23vr","passcode":"<设备码>"}]}'
{ "failed": [], "success": ["DT0LhnOwjSC7om2Yyb23vr"] }

机智云注册绑定

绑定成功。同一个设备可以同时被多个账号绑定——所以微信小程序完全不受影响,而我们的 HA 从此有了自己的合法通道。这就是"正门":不破解、不绕过,用平台自己的规则把设备接管过来。

💡 为什么这套方法能推广?
因为机智云、涂鸦这些平台的"一机多绑"是产品设计如此(为了家人共享)。只要你的家电小程序接的是这类平台,appid 一换,整套流程原样能跑。

五、验证:用 30 行 Python 直连设备

写个最小可用脚本验证全链路(WebSocket → 登录 → 订阅 → 读状态):

WebSocket 实时数据

# 伪代码版(完整版见文末)
ws = connect("wss://m2m.gizwits.com:8880/ws/app/v1")
ws.send({"cmd": "login_req", "data": {appid, uid, token, ...}})
ws.send({"cmd": "subscribe_req", "data": [{"did": did}]})
ws.send({"cmd": "c2s_read", "data": {"did": did}})

# 设备立刻回推全量状态:
# power=true, speed=3, pm25=0, co2=427, tempin=25, 滤芯剩余=96% ...

实时数据全部到手——和微信小程序看到的是同一个数据流。

写控制也只是换个命令:

ws.send({"cmd": "c2s_write", "data": {"did": did, "attrs": {"speed": 2}}})
# 设备风速从 3 档变 2 档,1 秒内回显确认 ✓

六、落地:打包成 Home Assistant 集成

协议通了,剩下就是体力活:把这些能力包装成 HA 自定义组件 glancetek_air。

文件结构(共 11 个文件,约 600 行,文末获取):

custom_components/glancetek_air/
├── manifest.json      # 集成元数据
├── const.py           # 数据点定义
├── __init__.py        # 入口:注册账号→绑定→连WS→加载平台
├── config_flow.py     # YAML 导入流程
├── gizwits_api.py     # 机智云 REST(匿名注册/续期/绑定)
├── gizwits_ws.py      # WebSocket 客户端(心跳/重连/收发)
├── fan.py             # 风机实体(开关/风速/预设)
├── sensor.py          # 9 个传感器
├── switch.py          # 4 个开关
├── climate.py         # 空调模块
└── lock.py            # 童锁

第 1 步:把组件复制进 HA

Docker 安装的 HA(目录换成你自己的):

docker cp custom_components/glancetek_air homeassistant:/config/custom_components/

第 2 步:在 configuration.yaml 里加 5 行

glancetek_air:
  phone_id: my-ha-001        # 你注册时用的 phone_id,随便起,别改来改去
  devices:
    - did: DT0LhnOwjSC7om2Yyb23vr
      passcode: "你的设备码"
      name: 格兰斯柯新风

第 3 步:重启 Home Assistant

docker restart homeassistant

集成启动时会自动完成三件事:匿名注册(幂等,同 phone_id 永远拿回同一个账号)→ 绑定设备 → 建立 WebSocket 长连接。

而这条连接由一个常驻自愈 supervisor 看护:WebSocket 深夜悄悄断了?它自己重连;token 失效被服务器拒登录?它当场匿名注册换新再连;断网/路由器重启?它按指数退避爬起来;甚至 HA 启动那一刻 DNS 还没就绪也没关系,它一直在后台重试到成功。为什么必须有它——见第八节末尾,那是一次"第二天早上实体全灰"的真实事故。

七、验收:21 个实体全部上线

重启完成后,打开 HA。看到那 21 个实体了吗?

集成列表

实体列表

▲ 我的 HA 概览页(演示账号视角)

打开开发者工具 → 状态,输入 ge_lan 过滤:

开发者工具实体列表

PM2.5 详情
▲ PM2.5 实时为 0——新风干得不错

滤芯寿命
▲ 滤芯剩余 96%,这个数据点微信里都要点三层菜单才能看到,现在进自动化随取随用

最后,激动人心的控制实测——从 HA 里把风速从 3 档调到 2 档:

风机控制

curl -X POST http://localhost:8123/api/services/fan/set_preset_mode \
  -H "Authorization: Bearer <你的HA令牌>" \
  -d '{"entity_id":"fan.ge_lan_si_ke_xin_feng_feng_ji","preset_mode":"2档"}'

设备真的动了,面板 1 秒内回显 2档 67%。再调回 3 档、切智能模式、拖滑条低中高……全部通过(完整回归矩阵见第九节)。

定时开关机也测一发——微信"更多"页里那个功能,现在是一条服务调用:

# 每天 08:00 定时开机(repeat 位掩码:127 = 每天)
curl -X POST http://localhost:8123/api/services/glancetek_air/set_timer \
  -H "Authorization: Bearer <你的HA令牌>" \
  -d '{"entity_id":"switch.ge_lan_si_ke_xin_feng_ding_shi_kai_ji","time":"08:00","repeat":127}'

定时开关立刻变为 on,属性显示 08:00 每天——和微信里手动设置的效果一模一样,设备回显确认。能耗传感器同样实测:累计 103.42 kWh / 今日 16.52 kWh / 昨日 40.54 kWh,还带 total_increasing 状态类,可以直接拖进 HA 能源面板看电费曲线。

从微信独占到 HA 原生实体,只花了一个晚上。

八、进阶玩法(有了实体之后)

有了这 16 个实体,HA 的全部能力瞬间对这台新风机敞开:

# 自动化 1:CO₂ 超标自动开新风
automation:
  - alias: "CO2超标开新风"
    trigger:
      - platform: numeric_state
        entity_id: sensor.ge_lan_si_ke_xin_feng_co2
        above: 800
    action:
      - service: fan.turn_on
        target: {entity_id: fan.ge_lan_si_ke_xin_feng_feng_ji}
      - service: fan.set_percentage
        target: {entity_id: fan.ge_lan_si_ke_xin_feng_feng_ji}
        data: {percentage: 80}

# 自动化 2:滤芯寿命低于 10% 推送到手机
  - alias: "滤芯该换了"
    trigger:
      - platform: numeric_state
        entity_id: sensor.ge_lan_si_ke_xin_feng_lu_xin_shou_ming
        below: 10
    action:
      - service: notify.mobile_app
        data: {message: "新风滤芯只剩 10% 了,记得买!"}

再配上小爱同学网关、Apple Watch、AI 语音助手——"关一下新风"从此不需要解锁手机打开微信找到小程序再点一下。

九的前传:第四次翻车,就发生在接入后的第二天早上

能控制只是及格线,能长期无人值守地跑才是集成的毕业考。而这第四个坑,
不是理论推演——是我在自己身上踩的:

接入当晚一切完美。第二天早上打开 HA,21 个实体全部 unavailable。

没有任何报错弹窗。CO₂、能耗、温度的记录曲线在昨夜某刻集体停摆——可能是家里网络
抖了一下,可能是机智云服务器踢掉了长时间空闲的连接(它的心跳间隔参数是 180 秒,
但服务器端的断连阈值明显更苛刻)。而我最初的实现是"启动时连一次,断线后就静静
躺在那里"——WebSocket 一断,整个集成就变成了植物人。

只能手动点"重新加载实体"救活。这暴露出两个设计缺陷:

  1. 断线后无重连——一次网络抖动就是一次宕机,恢复全靠人肉;
  2. HA 有个特性火上浇油:entry 初始化失败后不会自动重试,日志里静悄悄的,
    唯一的提示是实体变灰。你的自动化、语音、历史曲线全部陪葬。

于是给集成装上了自愈 supervisor(常驻看护循环):

故障场景supervisor 的自愈动作
WS 深夜悄悄断开(本事故元凶)指数退避重连(10s→20s→40s…上限 10 分钟),爬起来后补拉一次全量状态
token 失效,登录被拒当场匿名注册(幂等,同一 phone_id 永远拿回同一账号)→ 换新 token → 立即重连
token 还剩 7 天到期心跳循环主动刷新,不给过期留机会(63 天有效期的长尾风险也顺手覆盖)
HA 启动时 DNS 未就绪setup 不再 fail-fast,后台持续重试(不再需要手动重启 HA)

验证一(主动出击):故意用一个失效 token 启动连接——日志原文:

登录失败(token 可能过期): Gizwits login failed: {'cmd': 'login_res', 'data': {'success': False}} → 尝试刷新 token
  >>> reauth 触发:真实匿名注册刷新 token
Gizwits WS 已连接并订阅
  收到实时数据: {'co2': 465, 'tvoc': 221, 'energyA': 13576, ...}

失效 token → 自动刷新 → 重连 → CO₂/能耗数据实时恢复,全程 40 秒,零人工介入。

验证二(自愈现场直播):装上 supervisor 的当天早上,家里的网络真的抖了——
正好全程记录了它的自愈过程:

09:21:55  登录失败(token 可能过期)→ 尝试刷新 token
09:22:06  token 刷新失败: [Errno -3](DNS 也在抖),10s 后重试
09:22:23  WS 断开,20s 后重连
09:25:56  token 刷新失败: [Errno -3],10s 后重试
09:27:29  WS 断开,160s 后重连
(DNS 恢复)→ 登录成功 → 数据恢复 → 控制指令正常下发回显

同样是一场昨夜级别的网络抖动——上次结局是"第二天早上实体全灰",这次结局是
supervisor 自己爬起来,期间没人碰过 HA。从今往后这台新风的记录曲线,理论上可以
一直画到设备退休。

九、踩坑实录:四次翻车(语音联调三次 + 一夜之间实体全灰)

到这里实体都齐了,按说该收工了。但真正的"最后一公里"才刚开始——接入语音助手(GLM)后,我连续翻了三次车。这部分过程太真实了,专门记下来,因为每个坑都是通用经验。

第一次翻车:fan.set_speed not found

对着语音助手说"新风风量调到2档",它准确地识别了设备、确认了目标,然后——

语音报错

▲ 识别全对,执行 404:Action fan.set_speed not found

fan.set_speed 这个服务,HA 在 2022 年 10 月就弃用、随后彻底移除了,现在只有 fan.set_percentage 和 fan.set_preset_mode。但大模型的训练语料里还满是旧文档,它"背旧书"生成了一个不存在的调用。

当时我的临时解法:给实体加"标签"硬教 LLM——

标签提示词
▲ 在实体标签里手写档位映射:1档=20%、2档=40%……

你从截图里还能看出当时埋的雷:标签里写了 5 档映射(4档=80%、5档=100%),而这台机器物理上只有 3 档。百分比控制确实生效了,但"2档"这种说法始终时灵时不灵。

根治(两个动作):

  1. 把档位做成实体原生能力——preset_modes: ['手动', '智能', '1档', '2档', '3档'],LLM 从实体属性里就能"看见"档位,直接命中 fan.set_preset_mode;
  2. 注册兼容服务 glancetek_air.set_speed,就算 LLM 再生成旧风格调用也有兜底。

第二次翻车:切档位,新风模式被"顺手"改了

用预设模式切 2 档,风量确实变了——但打开微信小程序一看,模式从"内循环"变成了"新风":

小程序实际状态
▲ 调完风量后微信里的实际状态:模式被切到了"新风",而我只让它改风量

而用滑条的"高/中/低"调就没这个问题。同一台设备,两条路径行为不一致——这个现象本身就是最好的线索。

排查发现:我上一版在切档指令里捎带了 auto: false(想着"切档同时退出智能模式",看似合理)。但这台设备的固件有个联动陷阱:写指令里只要出现 auto 字段,就会连带修改 fresh(内/外循环)。滑条路径我只发 speed,所以干净。

翻回微信 H5 源码求证——原版切风速时只发 {"speed": n} 一个字段,从不碰 auto。照抄原版语义,问题消失。

💡 给所有逆向者的一条铁律:能抄原版行为就别自己发挥。固件的联动怪癖永远比你想的多。

第三次翻车:滑条的"中"档,落在了 3 档上

修复副作用后做回归测试,发现 HA 风机面板的"低/中/高"三档错位:"中"档实际是 2 档和 3 档,永远摸不到 1 档:

HA 风机面板
▲ HA 风机面板:振荡档"高/中/低/关闭"。当时"中"落 3 档、"低"落 2 档

根因是两套映射体系打架:我按协议 5 档做百分比(1档=20%),但 speed_count=3 让 HA 面板按 3 档均分(低=33% 中=67% 高=100%)。67% 进我的 5 档换算 ceil 出 3 档,跟"高"重叠;20% 这个档位在 HA 3 档体系里压根不存在。

修复:百分比改为最近邻对齐 HA 振荡档——33%→1档、67%→2 档、100%→3 档,滑条三档与设备一一对应。回归实测全绿:

操作结果新风模式被误动?
预设切 2 档2 档(67%)否 ✅
面板"低"1 档否 ✅
面板"中"2 档否 ✅
面板"高"3 档否 ✅
预设"智能"智能模式(唯一动 auto 的入口)预期内

语音成功
▲ 三次翻车修完:语音"调到2档"“调到40%"全部一次命中

顺带说明:面板上的"关闭"按钮是直接关机(HA fan 域的标准语义,0%→turn_off→电源关闭),这台设备也不存在"风速归零但开机"的状态——和微信里的电源键行为一致,属正常。

静态排坑清单(另外几条雷)

  1. 小程序包解不开是正常的——V1MMWX 加密跟账号绑定,别死磕。走内存路线。
  2. 内存扫描必须用 sudo——普通用户连 /proc/pid/mem 都打不开。
  3. 小程序要处于打开状态——关掉小程序 token 就可能被回收,扫不到。
  4. 机智云的服务器 TLS 很老——只支持 TLS1.2 + AES128-SHA,Python 连接时必须放宽安全级别:
    ctx.set_ciphers("DEFAULT:@SECLEVEL=1")   # 否则握手失败
    
  5. WebSocket 帧超过 126 字节要用扩展长度——标准要求,很多简易实现会漏,导致服务器报 parameters format error。
  6. HA 新版 API 变了——async_forward_platforms 已改名 async_forward_entry_setups;实体必须设 unique_id 否则每次重启重复注册;自定义服务必须在 services.yaml 声明;cv.percentage 校验器已不存在。
  7. 常驻任务别用 hass.async_create_task——会被 HA bootstrap 当作 setup 前台任务等待,拖慢启动收尾。用 loop.create_task 挂后台,并在 EVENT_HOMEASSISTANT_STOP 时清理,两处日志警告("Setup timed out" 和 "still running after final writes")才能归零。
  8. 连接建立后要主动补一次 c2s_read——setup 时发的全量读取可能落在连接建立之前,直接发进了断开的 socket(静默丢弃),实体会一直 unknown。把它挪到"订阅成功"之后发,问题消失。
  9. 本机型风速实际只有 3 档——协议支持 1~5,但 capability 数据点会告诉你真实上限(我的机器全是 0,即 3 档封顶)。发 4 档设备会静默忽略,和微信里行为一致,属正常。

十、写在最后:这套方法论的边界

复盘一下,这次逆向没有破解任何加密、没有伪造任何身份、没有碰设备固件一个字节。我们做的三件事——读自己电脑上自己进程的内存、下载公开网页的源码、调用物联网平台文档里的官方接口——全部在"分析自家设备如何工作"的正当范围内,本质上是把厂商藏在微信后面的能力,用平台自己提供的方式重新暴露出来。

这个方法论的适用边界也很清晰:

  • ✅ 适用:小程序接的是机智云/涂鸦等公开物联网平台的家电(这样的白电非常多:某新风、某净水器、某取暖器、某晾衣架……)
  • ❌ 不适用:厂商完全自建云、协议私有加密、或者蓝牙直连不走云的设备

最后一句话总结这次的经历:

当一个家电厂商把"智能"做成微信里的孤岛时,逆向不是黑客行为,而是用户拿回自己设备控制权的正当手段。


资源获取

本文提到的所有代码,关注后回复「新风」获取:
公众号
或者微信搜索 开元俱乐部

  • glancetek_air 完整 HA 集成组件(开箱即用,含 token 自愈/断线重连 supervisor)
  • gizwits_hunter.py 一键抓取脚本(本文第二节那个)
  • 逆向过程笔记(含机智云 WS 协议完整格式)

如果你家的设备不是格兰斯柯,同样可以试试 gizwits_hunter.py——只要它接的是机智云平台,90% 的步骤完全一样。跑一遍脚本,看能不能抓到 login_req,能抓到就说明这条路走得通。

定时自动化的想象空间:

# 工作日早上 7 点半自动开新风(比设备定时更强:还能加条件)
automation:
  - alias: "工作日晨间新风"
    trigger: { platform: time, at: "07:30:00" }
    condition:
      - condition: time, weekday: [mon, tue, wed, thu, fri]
    action:
      - service: glancetek_air.set_timer
        data: { entity_id: switch...定时关机, time: "22:00", repeat: 127 }

觉得有用的话,转发给那个总抱怨"又要开微信关空调"的家人吧 😄


原创不易,转载请注明出处。设备型号:格兰斯柯 JWDCAC001 新风一体机;HA 版本 2026.10;实测日期 2026-11-05~07(含功耗/定时/语音联调三轮排错与 token 自愈验证)。、

Q.E.D.


寻门而入,破门而出