问题描述
Classic 平台的 MQTT 连接异常断开时,libdeye 会在 Paho 的 on_disconnect 回调中刷新 MQTT 连接信息。
如果刷新过程中发生临时 DNS 超时或德业云 API 连接异常,异常会通过 Future.result() 重新抛回 Paho 网络线程,最终导致该线程永久退出。
发生后会出现:
- Home Assistant 不再收到设备状态;
- Home Assistant 无法控制设备;
- HA 中仍可能显示之前缓存的状态;
- 官方 App 状态和控制正常;
- 新启动的
deye-cli 可以查询和控制同一设备;
- 只有重新加载集成、创建新的 MQTT Client 后才能恢复。
环境信息
libdeye:2.1.5
ha-deye-dehumidifier:2.1.7
- Home Assistant:2025.6.1
- 安装方式:Home Assistant Container
- Python:3.13
- 设备型号:DYD-W20A3-京鱼座
- 设备平台:Classic
相关代码
当前异常断线处理逻辑:
def _mqtt_on_disconnect(
self,
_mqtt: mqtt.Client,
_userdata: None,
result_code: int,
) -> None:
if result_code == 0:
return
asyncio.run_coroutine_threadsafe(
self._set_mqtt_info(),
self._loop,
).result()
这个回调运行在 Paho MQTT 网络线程中。
Future.result() 存在两个问题:
- 它会同步阻塞 Paho 网络线程,直到云 API 请求完成;
_set_mqtt_info() 抛出的异常会通过 result() 重新抛到 Paho 网络线程。
如果异常没有被捕获,Paho 网络线程会直接结束,后续无法再执行自动重连、心跳、消息接收和消息发送。
实际日志
ERROR (paho-mqtt-client-) [root] Uncaught thread exception
aiodns.error.DNSError:
(12, 'Timeout while contacting DNS servers')
aiohttp.client_exceptions.ClientConnectorDNSError:
Cannot connect to host api.deye.com.cn:443
[Timeout while contacting DNS servers]
File ".../libdeye/mqtt_client.py", line 82,
in _mqtt_on_disconnect
asyncio.run_coroutine_threadsafe(
self._set_mqtt_info(),
self._loop,
).result()
libdeye.cloud_api.DeyeCloudApiCannotConnectError
堆栈最终结束于:
threading.Thread._bootstrap_inner
说明异常从 Paho 回调中逃逸,并终止了 MQTT 网络线程。
复现方式
- 使用
DeyeClassicMqttClient 连接 Classic 平台设备;
- 等待 MQTT 正常连接并收发消息;
- 触发一次非主动 MQTT 断线;
- 在
on_disconnect 刷新 MQTT 信息期间,让 api.deye.com.cn 出现 DNS 超时或临时不可访问;
- 日志中出现
Uncaught thread exception;
- 恢复网络或 DNS;
- 原有 MQTT Client 仍然无法恢复;
- 新启动的
deye-cli 可以连接和控制同一设备;
- 重新加载 HA 集成后恢复。
期望行为
临时 DNS 或云 API 请求失败应该被记录,但不能:
- 无限阻塞 Paho 网络线程;
- 让异常逃逸到 Paho 回调线程;
- 导致 MQTT 网络循环永久退出。
刷新失败后,Paho 网络线程应该继续运行,并保留后续自动重连的能力。
实际行为
Future.result() 将 DeyeCloudApiCannotConnectError 重新抛入 Paho 网络线程,导致 MQTT 网络循环永久终止。
建议修复方案
建议:
- 不要在 Paho 回调中同步等待
Future.result();
- 将 MQTT 信息刷新任务提交到 asyncio 事件循环后立即返回;
- 为刷新任务设置有限超时,例如 15 秒;
- 捕获 DNS、云 API 和其他刷新异常;
- 避免断线期间重复创建多个刷新任务;
- 主动断开客户端时取消尚未完成的刷新任务;
- 增加以下测试:
- 断线回调不会调用
Future.result();
- 云 API 异常不会逃逸到 Paho 网络线程;
- 刷新超时会被正确处理;
- 多次断线回调不会创建重复刷新任务。
本地已经实现并验证了上述方案,libdeye 的 116 个测试全部通过。
如果维护者认可这个处理方式,我可以继续提交对应 PR。
问题描述
Classic 平台的 MQTT 连接异常断开时,
libdeye会在 Paho 的on_disconnect回调中刷新 MQTT 连接信息。如果刷新过程中发生临时 DNS 超时或德业云 API 连接异常,异常会通过
Future.result()重新抛回 Paho 网络线程,最终导致该线程永久退出。发生后会出现:
deye-cli可以查询和控制同一设备;环境信息
libdeye:2.1.5ha-deye-dehumidifier:2.1.7相关代码
当前异常断线处理逻辑:
这个回调运行在 Paho MQTT 网络线程中。
Future.result()存在两个问题:_set_mqtt_info()抛出的异常会通过result()重新抛到 Paho 网络线程。如果异常没有被捕获,Paho 网络线程会直接结束,后续无法再执行自动重连、心跳、消息接收和消息发送。
实际日志
堆栈最终结束于:
说明异常从 Paho 回调中逃逸,并终止了 MQTT 网络线程。
复现方式
DeyeClassicMqttClient连接 Classic 平台设备;on_disconnect刷新 MQTT 信息期间,让api.deye.com.cn出现 DNS 超时或临时不可访问;Uncaught thread exception;deye-cli可以连接和控制同一设备;期望行为
临时 DNS 或云 API 请求失败应该被记录,但不能:
刷新失败后,Paho 网络线程应该继续运行,并保留后续自动重连的能力。
实际行为
Future.result()将DeyeCloudApiCannotConnectError重新抛入 Paho 网络线程,导致 MQTT 网络循环永久终止。建议修复方案
建议:
Future.result();Future.result();本地已经实现并验证了上述方案,
libdeye的 116 个测试全部通过。如果维护者认可这个处理方式,我可以继续提交对应 PR。