中枢网关一次 getDevList 超时后本地通道永久失效:get_dev_list_async 抛异常逃逸,使「Retry until success」成为死代码(相关 #1679)

Author: ChrisFengACreated Sep 10, 2026Updated Sep 10, 2026

相关:#1679 报告了同样的现象(BE6500PRO 内置中枢不回 getDevList),并且 @vshijiav 已经查明中枢侧的根因 —— 中枢 miot_central 容器里的 proxy_service 在开机初始化阶段(Mesh 回程尚未建立时)死锁挂起,之后所有 MIPS 请求全部超时。

本 issue 想补充的是集成侧的问题:即使中枢侧的 bug 由小米修好,集成现在的处理方式也会把「中枢一次抽风」放大成「本地通道永久失效且完全静默」。另外补充一个数据点:这个故障不只发生在 BE6500PRO 上,独立的 xiaomi.gateway.hub1(固件 3.5.0_0011)同样会

Describe the Bug / 描述问题

中枢网关一旦有一次 proxy/getDevList 不回应,本地中枢通道就会永久失效,而且不会有任何告警,直到重启 Home Assistant。

我的中枢是独立的 xiaomi.gateway.hub1(固件 3.5.0_0011,有线接入)。它已连续运行 8.8 天、未死机、米家 App 控制一切正常(App 走云端),但对 HA 的本地 MIPS 请求一律不回。我绕开 HA 用 paho-mqtt 按同样的协议直连中枢复现了一遍:TLS 客户端证书握手成功、MQTT CONNACK Success(换一个没注册的 client_id 会得到 Not authorized,说明白名单在正常工作)、订阅 <virtual-did>/# 拿到 Granted QoS 2、QoS 2 的 PUBLISH 收到完整 PUBCOMP —— 但 master/proxy/getDevListmaster/proxy/getmaster/proxy/getMijiaActionGroupList 全部零回复。也就是 broker 层正常、proxy_service 那一层不应答,与 #1679 里的结论一致。重启中枢后立即恢复。

MIoTClient.__refresh_gw_devices_with_group_id_async() 里:

gw_list: dict = await mips.get_dev_list_async(
    payload=json.dumps(payload))
if gw_list is None:
    _LOGGER.error(
        'refresh gw devices with group_id failed, %s, %s',
        self._uid, group_id)
    # Retry until success
    self.__request_refresh_gw_devices_by_group_id(
        group_id=group_id)
    return

MipsLocalClient.get_dev_list_async() 在超时时是 raise MIoTMipsError('invalid result'),不是返回 None

result_obj = await self.__request_async(
    topic='proxy/getDevList', payload=payload or '{}', timeout_ms=timeout_ms)
if not result_obj or 'devList' not in result_obj:
    raise MIoTMipsError('invalid result')

调用方没有 try/except,异常从 create_task() 里逃逸(Home Assistant 记录为 Task exception was never retrieved),因此 if gw_list is None: 这段「Retry until success」是死代码,永远不会执行

后果:_device_list_gateway 里所有设备永远是 online: False。而 __check_device_state(cloud_state, gw_state, lan_state) 是三者取或,于是设备可用性完全依赖云端 MQTT。云端 MQTT 每次断线重连(mips disconnect, 7),整个账号下所有在线实体一起变成 unavailable,而中枢明明就在局域网里、连接也是正常的。

我这边的规模:一次云端断线让 1465 个实体同时 unavailable,约十几秒后恢复,每天 5 次左右。

为什么这个失败是持久的__request_refresh_gw_devices_by_group_id() 全文只有两个调用点,且 gateway 设备表没有任何周期性刷新定时器(云端设备表、props、token、cert 都各有 call_later 定时器):

  1. 上面那段死代码;
  2. __on_mips_local_state_changed(state=True) —— 本地 MQTT 重连时。但本地是局域网连接(RTT ~1.6 ms),实测数天不断一次。

另外 __on_gw_device_list_changed() 里的 get_dev_list_async() 也没有 try/except,同样会抛异常,而且它只对变化的 did 做增量更新,无法重建整张表。

所以实际效果就是:中枢一次没回应 → 本地通道永久失效,直到重启 Home Assistant。

How to Reproduce / 复现步骤

  1. 一个配置了中枢网关(xiaomi.gateway.hub1)的家庭,ctrl_mode: auto
  2. 让中枢在集成建立本地 MIPS 连接后的那一次 master/proxy/getDevList 请求上不回应(我这里是中枢的本地代理服务自己卡住了:MQTT broker、TLS 客户端证书校验、client_id 白名单、topic ACL 全部正常,QoS 2 的 PUBLISH 也收到了 PUBCOMP,但 master/proxy/* 的任何请求都不回;proxy/getDevListproxy/getproxy/getMijiaActionGroupList 都试过,全部零回复。中枢已连续运行 8.8 天,未死机,米家 App 控制设备完全正常,因为 App 走的是云端)。
  3. 日志里出现 on mips request timeout, ..., master/proxy/getDevList,紧接着一条 Task exception was never retrieved + MIoTMipsError: invalid result
  4. 此后不会再有任何 refresh gw devices with group_id 的重试,除非重启 Home Assistant。
  5. 等下一次云端 MQTT 断线 —— 该账号下所有实体一起 unavailable

补充:我把超时从 10 秒临时改到 45 秒重测,中枢照样不回,所以这不是超时值调小了的问题。而中枢重启后刚起来时getDevList 需要 23–34 秒才返回(热起来之后 0.1 秒返回 64 个设备)。这意味着如果 Home Assistant 和中枢差不多时间一起重启(停电、固件升级),默认 10 秒超时几乎必然超时 —— 此时「能不能重试」比「超时值多大」重要得多。

Expected Behavior / 预期结果

get_dev_list_async() 失败时应被捕获,让已经写好的「Retry until success」逻辑真正生效;中枢恢复后本地通道应能自动恢复,无需重启 Home Assistant。

Reproduce Time / 问题复现的时间点

2026-09-10 19:13:25

Home Assistant Logs / 系统日志

(已脱敏:<virtual-did> = 集成的 virtual_did,<group-id> = 家庭的 group_id,<uid> = 小米账号 uid,<uuid> = 集成实例 uuid)

集成加载后,本地 MIPS 连上 → 请求全量设备表 → 10 秒超时 → 异常逃逸,之后再无重试:

2026-09-10 19:13:12.276 INFO  [custom_components.xiaomi_home.miot.miot_client] local mips state changed, <group-id>, True
2026-09-10 19:13:15.280 DEBUG [custom_components.xiaomi_home.miot.miot_client] refresh gw devices with group_id, <group-id>
2026-09-10 19:13:15.292 DEBUG [custom_components.xiaomi_home.miot.miot_client] <home-name>, mips local call api, True, 620821611, master/proxy/getDevList, {"info": ["name", "model", "urn", "online", "specV2Access", "pushAvailable"]}
2026-09-10 19:13:25.295 ERROR [custom_components.xiaomi_home.miot.miot_client] <home-name>, on mips request timeout, 620821611, master/proxy/getDevList, {"info": ["name", "model", "urn", "online", "specV2Access", "pushAvailable"]}
2026-09-10 19:13:25.296 ERROR [homeassistant] Error doing job: Task exception was never retrieved (task: None)
    raise MIoTMipsError('invalid result')
custom_components.xiaomi_home.miot.miot_error.MIoTMipsError: invalid result

云端 MQTT 断线,导致全部实体 unavailable(因为 gateway 兜底失效):

2026-09-10 18:21:43.447 ERROR [custom_components.xiaomi_home.miot.miot_client] ha.<uuid>, mips disconnect, 7, None
2026-09-10 18:21:43.448 ERROR [custom_components.xiaomi_home.miot.miot_client] ha.<uuid>, mips try reconnect after 10s

同一秒内 1465 个实体转为 unavailable(用 recorder 数据库按秒聚合统计得到)。

__on_gw_device_list_changed() 里同样的未捕获异常:

  File "/config/custom_components/xiaomi_home/miot/miot_client.py", line 1268, in __on_gw_device_list_changed
    gw_list = await mips.get_dev_list_async(
  File "/config/custom_components/xiaomi_home/miot/miot_mips.py", line 1383, in get_dev_list_async
    raise MIoTMipsError('invalid result')
custom_components.xiaomi_home.miot.miot_error.MIoTMipsError: invalid result

Log Timezone / 日志时区

Australia/Sydney

Home Assistant Core Version / Home Assistant Core 版本

2026.9.1

Home Assistant Operation System Version / Home Assistant Operation System 版本

18.2

Xiaomi Home Integration Version / 米家集成版本

v0.4.7

Additional Context / 其他说明

本地临时补丁(已在我这里运行,中枢重启后可自动恢复,无需重启 Home Assistant):

        try:
            gw_list: dict = await mips.get_dev_list_async(
                payload=json.dumps(payload))
        except Exception as err:  # pylint: disable=broad-except
            fails = getattr(self, '_gw_refresh_fails', {})
            count = fails.get(group_id, 0) + 1
            fails[group_id] = count
            self._gw_refresh_fails = fails
            delay = min(300, 15 * count)
            _LOGGER.warning(
                'get gw device list failed (#%d), retry in %ds, %s, %s',
                count, delay, group_id, err)
            self._main_loop.call_later(
                delay, lambda: self._main_loop.create_task(
                    self.__refresh_gw_devices_with_group_id_async(
                        group_id=group_id)))
            return
        if gw_list is None:
            ...

加了退避(15s × 失败次数,上限 5 分钟)是为了避免中枢长期故障时刷屏 —— 默认的 REFRESH_GATEWAY_DEVICES_DELAY = 3 配上 10 秒超时会变成每 13 秒重试一次。

建议一并考虑:

  1. __on_gw_device_list_changed() 里同样的未捕获调用;
  2. 是否给 gateway 设备表加一个周期性刷新定时器(对齐云端设备表 / props / token / cert 的做法),这样本地通道失效时能自愈;
  3. 中枢刚启动时 getDevList 需要 20–35 秒,默认 10 秒超时对「HA 与中枢同时重启」这个很常见的场景偏紧。

English summary

MipsLocalClient.get_dev_list_async() raises MIoTMipsError on timeout instead of returning None, but MIoTClient.__refresh_gw_devices_with_group_id_async() only handles if gw_list is None:. With no try/except, the exception escapes the create_task() (Task exception was never retrieved), so the # Retry until success branch is dead code.

_device_list_gateway then stays all-online: False forever. Since __check_device_state() ORs cloud/gateway/lan, entity availability falls back to the cloud MQTT link alone — every cloud reconnect (mips disconnect, 7) turns all 1465 entities of the account unavailable for ~15 s, several times a day, even though the central hub is healthy on the LAN.

The failure is effectively permanent: __request_refresh_gw_devices_by_group_id() has only two call sites (the dead branch above, and local-MQTT reconnect), and there is no periodic refresh timer for the gateway device list — unlike cloud devices, props, token and cert, which all have one. The LAN MQTT link does not reconnect on its own for days. __on_gw_device_list_changed() has the same unguarded call and only does incremental updates, so it cannot rebuild the table either.

Raising the timeout does not help (tested 45 s — the hub still never answered). But note the hub needs 23–34 s to answer getDevList right after it boots (0.1 s once warm), so a simultaneous HA + hub restart will hit the 10 s default almost every time — which makes retrying far more important than the timeout value.