fix(mcp): 异常面适配真实 bleak——重试/错误码映射覆盖异常全家
- C1: _run_with_retry 捕获面 TransportError → (base+local 两个 TransportError,
BleakError, OSError, asyncio.TimeoutError);真实掉线 bleak 裸抛
BleakError("Not connected") 不再绕过重连重试
- C2/I1a: _map_transport_error 接受异常全家,类型优先(asyncio.TimeoutError、
BleakDeviceNotFoundError → CONNECT_TIMEOUT)再按中英文消息映射;_call /
scan_devices / device_status 的 except 同步扩展,device_status 不再把传输层
错误兜底成 INTERNAL
- I2: _ensure_connected 的 __aenter__ 半途失败补 __aexit__,防半开连接泄漏
- M1: status() 先算 idle_seconds 再 _touch(),读数不再被自身归零
- docstring 校准:scan_devices 返回形状 data.devices;device_status 注明
触发连接守候且刷新空闲计时
- 新测试 8 个(先红后绿):raw BleakError 重连成功/耗尽上抛、aenter 清理、
idle_seconds 语义、local 中文 TransportError 两条映射、raw BleakError 映射、
device_status 传输错误映射
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
1 parent
16dbf494c9
commit
1f78587bbd
4 files changed
+164
-15
No files matched your search
@@ -3,15 +3,27 @@
|
|||||||
- 一把 asyncio.Lock 串行所有设备操作(BLE 适配器独占)
|
- 一把 asyncio.Lock 串行所有设备操作(BLE 适配器独占)
|
||||||
- 首次操作守候连接(connect_timeout 应覆盖设备 30-60s 广播窗口)
|
- 首次操作守候连接(connect_timeout 应覆盖设备 30-60s 广播窗口)
|
||||||
- 操作后保持连接;空闲 idle_timeout 秒自动断开让设备睡眠
|
- 操作后保持连接;空闲 idle_timeout 秒自动断开让设备睡眠
|
||||||
- 操作中 TransportError(连接丢失)→ 重连并重试一次,再失败向上抛
|
- 操作中连接丢失(TRANSPORT_ERRORS 全家)→ 重连并重试一次,再失败向上抛
|
||||||
"""
|
"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import time
|
import time
|
||||||
|
|
||||||
|
from bleak.exc import BleakError
|
||||||
|
|
||||||
from .client import PPClient
|
from .client import PPClient
|
||||||
from .transports.base import TransportError
|
from .transports.base import TransportError
|
||||||
|
from .transports.local import TransportError as LocalTransportError
|
||||||
|
|
||||||
|
# 传输层异常全家(掉线/未连上的全部真实形态):
|
||||||
|
# - transports.base.TransportError:SDK 公开语义异常(bridge/fake 路径)
|
||||||
|
# - transports.local.TransportError:真实 BLE 路径 connect/scan/ID 应答的中文异常
|
||||||
|
# (SDK 两个同名类互不继承,历史包袱;MCP 层必须同时覆盖)
|
||||||
|
# - bleak.exc.BleakError:写/读路径 bleak 裸抛不包装(如 "Not connected")
|
||||||
|
# - OSError / asyncio.TimeoutError:bleak 后端平台差异形态
|
||||||
|
TRANSPORT_ERRORS = (TransportError, LocalTransportError, BleakError,
|
||||||
|
OSError, asyncio.TimeoutError)
|
||||||
|
|
||||||
|
|
||||||
class DeviceManager:
|
class DeviceManager:
|
||||||
@@ -43,12 +55,14 @@ class DeviceManager:
|
|||||||
async def status(self) -> dict:
|
async def status(self) -> dict:
|
||||||
async with self._lock:
|
async with self._lock:
|
||||||
await self._ensure_connected()
|
await self._ensure_connected()
|
||||||
|
# 先算 idle_seconds 再 _touch:status 自身也是活动,但不能把读数归零
|
||||||
|
idle = (None if self._last_activity is None
|
||||||
|
else round(time.monotonic() - self._last_activity, 1))
|
||||||
self._touch()
|
self._touch()
|
||||||
info = {
|
info = {
|
||||||
"connected": self.connected,
|
"connected": self.connected,
|
||||||
"mac": self._mac,
|
"mac": self._mac,
|
||||||
"idle_seconds": None if self._last_activity is None
|
"idle_seconds": idle,
|
||||||
else round(time.monotonic() - self._last_activity, 1),
|
|
||||||
"connect_count": self._connect_count,
|
"connect_count": self._connect_count,
|
||||||
"connect_timeout": self._connect_timeout,
|
"connect_timeout": self._connect_timeout,
|
||||||
"idle_timeout": self._idle_timeout,
|
"idle_timeout": self._idle_timeout,
|
||||||
@@ -71,7 +85,7 @@ class DeviceManager:
|
|||||||
await self._ensure_connected()
|
await self._ensure_connected()
|
||||||
try:
|
try:
|
||||||
return await op(self._client)
|
return await op(self._client)
|
||||||
except TransportError:
|
except TRANSPORT_ERRORS:
|
||||||
await self._drop()
|
await self._drop()
|
||||||
await self._ensure_connected()
|
await self._ensure_connected()
|
||||||
return await op(self._client)
|
return await op(self._client)
|
||||||
@@ -80,7 +94,16 @@ class DeviceManager:
|
|||||||
if self._client is not None:
|
if self._client is not None:
|
||||||
return
|
return
|
||||||
client = self._factory(self._mac, self._connect_timeout)
|
client = self._factory(self._mac, self._connect_timeout)
|
||||||
await client.__aenter__()
|
try:
|
||||||
|
await client.__aenter__()
|
||||||
|
except Exception:
|
||||||
|
# __aenter__ 半途失败(如 connect 后 start_notify 炸)会留下半开连接,
|
||||||
|
# 补一次 __aexit__ 清理;清理自身失败不影响原异常上抛
|
||||||
|
try:
|
||||||
|
await client.__aexit__(None, None, None)
|
||||||
|
except Exception: # noqa: BLE001
|
||||||
|
pass
|
||||||
|
raise
|
||||||
self._client = client
|
self._client = client
|
||||||
self._connect_count += 1
|
self._connect_count += 1
|
||||||
if self._mac is None: # 自动扫描后记住实际地址
|
if self._mac is None: # 自动扫描后记住实际地址
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
"""
|
"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import asyncio
|
||||||
import base64
|
import base64
|
||||||
import binascii
|
import binascii
|
||||||
import datetime
|
import datetime
|
||||||
@@ -12,9 +13,10 @@ import io
|
|||||||
import os
|
import os
|
||||||
|
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
|
from bleak.exc import BleakDeviceNotFoundError
|
||||||
|
|
||||||
from .client import PPClient
|
from .client import PPClient
|
||||||
from .transports.base import TransportError
|
from .device_manager import TRANSPORT_ERRORS
|
||||||
|
|
||||||
_TIMEOUT_HINT = "设备长睡眠、广播窗口 30-60s;调大 --connect-timeout 或稍后重试"
|
_TIMEOUT_HINT = "设备长睡眠、广播窗口 30-60s;调大 --connect-timeout 或稍后重试"
|
||||||
_NOTFOUND_HINT = "未发现 NRF- 前缀设备;确认设备在位且未处于深睡"
|
_NOTFOUND_HINT = "未发现 NRF- 前缀设备;确认设备在位且未处于深睡"
|
||||||
@@ -49,10 +51,23 @@ def _err(tool: str, code: str, message: str, hint: str | None = None) -> dict:
|
|||||||
return {"ok": False, "tool": tool, "error": error}
|
return {"ok": False, "tool": tool, "error": error}
|
||||||
|
|
||||||
|
|
||||||
def _map_transport_error(tool: str, exc: TransportError) -> dict:
|
def _map_transport_error(tool: str, exc: Exception) -> dict:
|
||||||
|
"""传输层异常全家(TRANSPORT_ERRORS)→ 统一错误码。
|
||||||
|
|
||||||
|
真实硬件路径的异常来源:SDK 语义 TransportError(base/local 两个同名类)、
|
||||||
|
bleak 裸抛 BleakError、平台层 OSError / asyncio.TimeoutError。
|
||||||
|
类型判定优先于消息匹配(BleakDeviceNotFoundError 的英文消息含 "not found",
|
||||||
|
但语义是"已知 MAC 守候窗口内未出现",归 CONNECT_TIMEOUT 而非 DEVICE_NOT_FOUND)。
|
||||||
|
"""
|
||||||
msg = str(exc)
|
msg = str(exc)
|
||||||
low = msg.lower()
|
low = msg.lower()
|
||||||
if "timeout" in low or "timed out" in low:
|
if isinstance(exc, (asyncio.TimeoutError, BleakDeviceNotFoundError)):
|
||||||
|
return _err(tool, "CONNECT_TIMEOUT", msg, _TIMEOUT_HINT)
|
||||||
|
if "超时" in msg or "timeout" in low or "timed out" in low:
|
||||||
|
return _err(tool, "CONNECT_TIMEOUT", msg, _TIMEOUT_HINT)
|
||||||
|
if "未找到" in msg:
|
||||||
|
return _err(tool, "DEVICE_NOT_FOUND", msg, _NOTFOUND_HINT)
|
||||||
|
if "连接失败" in msg: # 已知 MAC 守候未连上(bleak not found 的包装形态)
|
||||||
return _err(tool, "CONNECT_TIMEOUT", msg, _TIMEOUT_HINT)
|
return _err(tool, "CONNECT_TIMEOUT", msg, _TIMEOUT_HINT)
|
||||||
if "not found" in low or "no device" in low:
|
if "not found" in low or "no device" in low:
|
||||||
return _err(tool, "DEVICE_NOT_FOUND", msg, _NOTFOUND_HINT)
|
return _err(tool, "DEVICE_NOT_FOUND", msg, _NOTFOUND_HINT)
|
||||||
@@ -66,7 +81,7 @@ def register_tools(mcp, manager) -> None:
|
|||||||
try:
|
try:
|
||||||
data = await manager.run(op)
|
data = await manager.run(op)
|
||||||
return {"ok": True, "tool": tool, "data": data if data is not None else {}}
|
return {"ok": True, "tool": tool, "data": data if data is not None else {}}
|
||||||
except TransportError as exc:
|
except TRANSPORT_ERRORS as exc:
|
||||||
return _map_transport_error(tool, exc)
|
return _map_transport_error(tool, exc)
|
||||||
except ValueError as exc:
|
except ValueError as exc:
|
||||||
return _err(tool, "INVALID_PARAM", str(exc))
|
return _err(tool, "INVALID_PARAM", str(exc))
|
||||||
@@ -75,20 +90,26 @@ def register_tools(mcp, manager) -> None:
|
|||||||
|
|
||||||
@mcp.tool()
|
@mcp.tool()
|
||||||
async def scan_devices(timeout: float = 5.0) -> dict:
|
async def scan_devices(timeout: float = 5.0) -> dict:
|
||||||
"""扫描附近 NRF- 前缀墨水屏设备,返回 [{name,address,rssi}]。"""
|
"""扫描附近 NRF- 前缀墨水屏设备。返回形状:data.devices = [{name,address,rssi}]。"""
|
||||||
try:
|
try:
|
||||||
devices = await PPClient.scan_devices("local", timeout=timeout)
|
devices = await PPClient.scan_devices("local", timeout=timeout)
|
||||||
return {"ok": True, "tool": "scan_devices", "data": {"devices": devices}}
|
return {"ok": True, "tool": "scan_devices", "data": {"devices": devices}}
|
||||||
except TransportError as exc:
|
except TRANSPORT_ERRORS as exc:
|
||||||
return _map_transport_error("scan_devices", exc)
|
return _map_transport_error("scan_devices", exc)
|
||||||
except Exception as exc: # noqa: BLE001
|
except Exception as exc: # noqa: BLE001
|
||||||
return _err("scan_devices", "INTERNAL", f"{type(exc).__name__}: {exc}")
|
return _err("scan_devices", "INTERNAL", f"{type(exc).__name__}: {exc}")
|
||||||
|
|
||||||
@mcp.tool()
|
@mcp.tool()
|
||||||
async def device_status() -> dict:
|
async def device_status() -> dict:
|
||||||
"""查询连接状态/绑定 MAC/设备 ID/空闲秒数。"""
|
"""查询连接状态/绑定 MAC/设备 ID/空闲秒数。
|
||||||
|
|
||||||
|
注意:会触发连接守候(最长 --connect-timeout 秒)并刷新空闲计时——
|
||||||
|
周期轮询本工具会阻止 idle 自动断开。
|
||||||
|
"""
|
||||||
try:
|
try:
|
||||||
return {"ok": True, "tool": "device_status", "data": await manager.status()}
|
return {"ok": True, "tool": "device_status", "data": await manager.status()}
|
||||||
|
except TRANSPORT_ERRORS as exc:
|
||||||
|
return _map_transport_error("device_status", exc)
|
||||||
except Exception as exc: # noqa: BLE001
|
except Exception as exc: # noqa: BLE001
|
||||||
return _err("device_status", "INTERNAL", f"{type(exc).__name__}: {exc}")
|
return _err("device_status", "INTERNAL", f"{type(exc).__name__}: {exc}")
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
from bleak.exc import BleakError
|
||||||
|
|
||||||
from ppclock.client import PPClient
|
from ppclock.client import PPClient
|
||||||
from ppclock.device_manager import DeviceManager
|
from ppclock.device_manager import DeviceManager
|
||||||
@@ -15,6 +16,9 @@ class FakeTransport:
|
|||||||
self.exits = 0
|
self.exits = 0
|
||||||
self.rxtx_writes = []
|
self.rxtx_writes = []
|
||||||
self.fail_next_write = False
|
self.fail_next_write = False
|
||||||
|
# fail_next_write 触发时抛出的异常实例(默认 SDK 包装后的 TransportError;
|
||||||
|
# 真实硬件路径常是 bleak 裸抛的 BleakError,测试可替换)
|
||||||
|
self.fail_exc = TransportError("connection lost")
|
||||||
|
|
||||||
async def __aenter__(self):
|
async def __aenter__(self):
|
||||||
self.enters += 1
|
self.enters += 1
|
||||||
@@ -30,7 +34,7 @@ class FakeTransport:
|
|||||||
async def write_rxtx(self, data, response=True):
|
async def write_rxtx(self, data, response=True):
|
||||||
if self.fail_next_write:
|
if self.fail_next_write:
|
||||||
self.fail_next_write = False
|
self.fail_next_write = False
|
||||||
raise TransportError("connection lost")
|
raise self.fail_exc
|
||||||
self.rxtx_writes.append(data)
|
self.rxtx_writes.append(data)
|
||||||
|
|
||||||
async def read_rxtx(self):
|
async def read_rxtx(self):
|
||||||
@@ -139,6 +143,55 @@ class TestReconnect:
|
|||||||
await m.run(lambda dev: dev.set_mode("clock1"))
|
await m.run(lambda dev: dev.set_mode("clock1"))
|
||||||
await m.close()
|
await m.close()
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_reconnect_on_raw_bleak_error(self):
|
||||||
|
"""真实掉线 bleak 裸抛 BleakError(不经 SDK 包装),也要重连重试。"""
|
||||||
|
t1, t2 = FakeTransport(), FakeTransport()
|
||||||
|
t1.fail_next_write = True
|
||||||
|
t1.fail_exc = BleakError("Not connected")
|
||||||
|
m = make_manager([t1, t2])
|
||||||
|
await m.run(lambda dev: dev.set_mode("clock1"))
|
||||||
|
assert t1.enters == 1 and t2.enters == 1 # 掉线重连一次并成功
|
||||||
|
await m.close()
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_retry_exhaustion_raises_bleak_error(self):
|
||||||
|
"""重连后仍抛 BleakError → 原样上抛。"""
|
||||||
|
t1, t2 = FakeTransport(), FakeTransport()
|
||||||
|
for t in (t1, t2):
|
||||||
|
t.fail_next_write = True
|
||||||
|
t.fail_exc = BleakError("Not connected")
|
||||||
|
m = make_manager([t1, t2])
|
||||||
|
with pytest.raises(BleakError):
|
||||||
|
await m.run(lambda dev: dev.set_mode("clock1"))
|
||||||
|
await m.close()
|
||||||
|
|
||||||
|
|
||||||
|
class TestEnterFailure:
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_aenter_failure_cleans_up(self):
|
||||||
|
"""__aenter__ 抛异常时必须补 __aexit__(否则真实硬件泄漏半开连接)。"""
|
||||||
|
|
||||||
|
class HalfOpenClient:
|
||||||
|
def __init__(self):
|
||||||
|
self.exits = 0
|
||||||
|
|
||||||
|
async def __aenter__(self):
|
||||||
|
raise BleakError("connect boom")
|
||||||
|
|
||||||
|
async def __aexit__(self, *exc):
|
||||||
|
self.exits += 1
|
||||||
|
return False
|
||||||
|
|
||||||
|
client = HalfOpenClient()
|
||||||
|
m = DeviceManager(mac="AA:BB:CC:DD:EE:FF",
|
||||||
|
client_factory=lambda mac, timeout: client)
|
||||||
|
with pytest.raises(BleakError):
|
||||||
|
await m.run(lambda dev: dev.set_mode("clock1"))
|
||||||
|
assert client.exits == 1
|
||||||
|
assert not m.connected
|
||||||
|
await m.close()
|
||||||
|
|
||||||
|
|
||||||
class TestStatus:
|
class TestStatus:
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
@@ -150,5 +203,16 @@ class TestStatus:
|
|||||||
assert s["device_id"] == "81233F3C267112"
|
assert s["device_id"] == "81233F3C267112"
|
||||||
assert s["connect_timeout"] == 90.0
|
assert s["connect_timeout"] == 90.0
|
||||||
assert s["idle_timeout"] == 300.0
|
assert s["idle_timeout"] == 300.0
|
||||||
assert isinstance(s["idle_seconds"], float)
|
assert s["idle_seconds"] is None # 无先前活动;status 自身不得把读数归零
|
||||||
|
await m.close()
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_status_idle_seconds_reflects_last_activity(self):
|
||||||
|
"""status 自身的 _touch 不得把 idle_seconds 归零(先算后摸)。"""
|
||||||
|
t = FakeTransport()
|
||||||
|
m = make_manager([t])
|
||||||
|
await m.run(lambda dev: dev.set_mode("clock1"))
|
||||||
|
await asyncio.sleep(0.12)
|
||||||
|
s = await m.status()
|
||||||
|
assert s["idle_seconds"] >= 0.1
|
||||||
await m.close()
|
await m.close()
|
||||||
+42
-1
@@ -4,6 +4,7 @@ import io
|
|||||||
import json
|
import json
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
from bleak.exc import BleakError
|
||||||
from mcp.server.fastmcp import FastMCP
|
from mcp.server.fastmcp import FastMCP
|
||||||
from mcp.shared.memory import create_connected_server_and_client_session
|
from mcp.shared.memory import create_connected_server_and_client_session
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
@@ -11,6 +12,7 @@ from PIL import Image
|
|||||||
from ppclock.client import PPClient
|
from ppclock.client import PPClient
|
||||||
from ppclock.mcp_tools import decode_image_source, register_tools
|
from ppclock.mcp_tools import decode_image_source, register_tools
|
||||||
from ppclock.transports.base import TransportError
|
from ppclock.transports.base import TransportError
|
||||||
|
from ppclock.transports.local import TransportError as LocalTransportError
|
||||||
|
|
||||||
|
|
||||||
class FakeTransport:
|
class FakeTransport:
|
||||||
@@ -47,9 +49,10 @@ class FakeTransport:
|
|||||||
class FakeManager:
|
class FakeManager:
|
||||||
"""模拟 DeviceManager:直接对 fake client 执行 op,或抛预置错误。"""
|
"""模拟 DeviceManager:直接对 fake client 执行 op,或抛预置错误。"""
|
||||||
|
|
||||||
def __init__(self, error=None):
|
def __init__(self, error=None, status_error=None):
|
||||||
self.client = PPClient(FakeTransport())
|
self.client = PPClient(FakeTransport())
|
||||||
self.error = error
|
self.error = error
|
||||||
|
self.status_error = status_error
|
||||||
self.ops = 0
|
self.ops = 0
|
||||||
|
|
||||||
async def run(self, op):
|
async def run(self, op):
|
||||||
@@ -59,6 +62,8 @@ class FakeManager:
|
|||||||
return await op(self.client)
|
return await op(self.client)
|
||||||
|
|
||||||
async def status(self):
|
async def status(self):
|
||||||
|
if self.status_error is not None:
|
||||||
|
raise self.status_error
|
||||||
return {"connected": True, "mac": "AA:BB:CC:DD:EE:FF",
|
return {"connected": True, "mac": "AA:BB:CC:DD:EE:FF",
|
||||||
"idle_seconds": 0.0, "connect_count": 1,
|
"idle_seconds": 0.0, "connect_count": 1,
|
||||||
"connect_timeout": 90.0, "idle_timeout": 300.0,
|
"connect_timeout": 90.0, "idle_timeout": 300.0,
|
||||||
@@ -144,6 +149,42 @@ class TestTools:
|
|||||||
assert r["error"]["code"] == "CONNECT_TIMEOUT"
|
assert r["error"]["code"] == "CONNECT_TIMEOUT"
|
||||||
assert "hint" in r["error"]
|
assert "hint" in r["error"]
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_local_not_found_mapped(self):
|
||||||
|
"""真实 BLE 扫描落空:local.TransportError(中文)→ DEVICE_NOT_FOUND。"""
|
||||||
|
m = FakeManager(error=LocalTransportError("未找到名称前缀 'NRF-' 的设备"))
|
||||||
|
r = await call(make_session_coro(m), "set_time", {})
|
||||||
|
assert r["ok"] is False
|
||||||
|
assert r["error"]["code"] == "DEVICE_NOT_FOUND"
|
||||||
|
assert "hint" in r["error"]
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_local_connect_failure_mapped(self):
|
||||||
|
"""已知 MAC 守候未连上(bleak 报 not found 被包装成"连接失败")→ CONNECT_TIMEOUT。"""
|
||||||
|
m = FakeManager(error=LocalTransportError(
|
||||||
|
"连接失败: Device with address AA:BB:CC:DD:EE:FF was not found."))
|
||||||
|
r = await call(make_session_coro(m), "set_time", {})
|
||||||
|
assert r["ok"] is False
|
||||||
|
assert r["error"]["code"] == "CONNECT_TIMEOUT"
|
||||||
|
assert "hint" in r["error"]
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_raw_bleak_error_mapped(self):
|
||||||
|
"""写路径 bleak 裸抛 BleakError(不包装)→ BLE_ERROR。"""
|
||||||
|
m = FakeManager(error=BleakError("Not connected"))
|
||||||
|
r = await call(make_session_coro(m), "set_time", {})
|
||||||
|
assert r["ok"] is False
|
||||||
|
assert r["error"]["code"] == "BLE_ERROR"
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_device_status_transport_error_mapped(self):
|
||||||
|
"""device_status 的传输层错误与其它工具同映射,不兜底 INTERNAL。"""
|
||||||
|
m = FakeManager(status_error=LocalTransportError("等待设备 ID 应答超时"))
|
||||||
|
r = await call(make_session_coro(m), "device_status", {})
|
||||||
|
assert r["ok"] is False
|
||||||
|
assert r["error"]["code"] == "CONNECT_TIMEOUT"
|
||||||
|
assert "hint" in r["error"]
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_device_status(self):
|
async def test_device_status(self):
|
||||||
r = await call(make_session_coro(FakeManager()), "device_status", {})
|
r = await call(make_session_coro(FakeManager()), "device_status", {})
|
||||||
|
|||||||
Reference in new issue
Block a user