Skip to content

Commit 8ff8d2f

Browse files
fix: adding keepalive to local connection
1 parent f00f48d commit 8ff8d2f

File tree

4 files changed

+41
-29
lines changed

4 files changed

+41
-29
lines changed

roborock/api.py

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
5252
from .util import unpack_list
5353

5454
_LOGGER = logging.getLogger(__name__)
55+
KEEPALIVE = 60
5556
QUEUE_TIMEOUT = 4
5657
SPECIAL_COMMANDS = [
5758
RoborockCommand.GET_MAP_V1,
@@ -89,16 +90,25 @@ def __init__(self, endpoint: str, devices_info: Mapping[str, RoborockDeviceInfo]
8990
self._endpoint = endpoint
9091
self._nonce = secrets.token_bytes(16)
9192
self._waiting_queue: dict[int, RoborockFuture] = {}
92-
self._status_listeners: list[Callable[[int, str], None]] = []
93+
self._last_device_msg_in = self.time_func()
94+
self._last_disconnection = self.time_func()
95+
self.keep_alive = KEEPALIVE
9396

94-
def add_status_listener(self, callback: Callable[[int, str], None]):
95-
self._status_listeners.append(callback)
97+
@property
98+
def time_func(self) -> Callable[[], float]:
99+
try:
100+
# Use monotonic clock if available
101+
time_func = time.monotonic
102+
except AttributeError:
103+
time_func = time.time
104+
return time_func
96105

97106
async def async_disconnect(self) -> Any:
98107
raise NotImplementedError
99108

100109
def on_message(self, messages: list[RoborockMessage]) -> None:
101110
try:
111+
self._last_device_msg_in = self.time_func()
102112
for data in messages:
103113
protocol = data.protocol
104114
if protocol == 102 or protocol == 4:
@@ -142,6 +152,19 @@ def on_message(self, messages: list[RoborockMessage]) -> None:
142152
except Exception as ex:
143153
_LOGGER.exception(ex)
144154

155+
def on_disconnect(self, exc: Optional[Exception]) -> None:
156+
self._last_disconnection = self.time_func()
157+
_LOGGER.warning("Roborock client disconnected")
158+
if exc is not None:
159+
_LOGGER.warning(exc)
160+
161+
def should_keepalive(self) -> bool:
162+
now = self.time_func()
163+
# noinspection PyUnresolvedReferences
164+
if now - self._last_disconnection > self.keep_alive**2 and now - self._last_device_msg_in > self.keep_alive:
165+
return False
166+
return True
167+
145168
async def _async_response(self, request_id: int, protocol_id: int = 0) -> tuple[Any, VacuumError | None]:
146169
try:
147170
queue = RoborockFuture(protocol_id)

roborock/cloud_api.py

Lines changed: 6 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,14 @@
1010

1111
import paho.mqtt.client as mqtt
1212

13-
from .api import SPECIAL_COMMANDS, RoborockClient, md5hex
13+
from .api import SPECIAL_COMMANDS, RoborockClient, md5hex, KEEPALIVE
1414
from .containers import RoborockDeviceInfo, UserData
1515
from .exceptions import CommandVacuumError, RoborockException, VacuumError
1616
from .roborock_future import RoborockFuture
1717
from .roborock_message import RoborockMessage, RoborockParser, md5bin
1818
from .typing import RoborockCommand
1919

2020
_LOGGER = logging.getLogger(__name__)
21-
MQTT_KEEPALIVE = 60
2221
CONNECT_REQUEST_ID = 0
2322
DISCONNECT_REQUEST_ID = 1
2423

@@ -49,8 +48,6 @@ def __init__(self, user_data: UserData, devices_info: Mapping[str, RoborockDevic
4948
self._endpoint = base64.b64encode(md5bin(rriot.k)[8:14]).decode()
5049
self._waiting_queue: dict[int, RoborockFuture] = {}
5150
self._mutex = Lock()
52-
self._last_device_msg_in = mqtt.time_func()
53-
self._last_disconnection = mqtt.time_func()
5451
self.update_client_id()
5552

5653
def __del__(self) -> None:
@@ -80,19 +77,16 @@ def on_connect(self, *args, **kwargs) -> None:
8077

8178
def on_message(self, *args, **kwargs) -> None:
8279
_, __, msg = args
83-
self._last_device_msg_in = mqtt.time_func()
8480
device_id = msg.topic.split("/").pop()
8581
messages, _ = RoborockParser.decode(msg.payload, self.devices_info[device_id].device.local_key)
8682
super().on_message(messages)
8783

8884
def on_disconnect(self, *args, **kwargs) -> None:
8985
try:
9086
_, __, rc, ___ = args
91-
self._last_disconnection = mqtt.time_func()
92-
message = f"Roborock mqtt client disconnected (rc: {rc})"
87+
super().on_disconnect(RoborockException(f"(rc: {rc})"))
9388
if rc == mqtt.MQTT_ERR_PROTOCOL:
9489
self.update_client_id()
95-
_LOGGER.warning(message)
9690
connection_queue = self._waiting_queue.get(DISCONNECT_REQUEST_ID)
9791
if connection_queue:
9892
connection_queue.resolve((True, None))
@@ -102,18 +96,10 @@ def on_disconnect(self, *args, **kwargs) -> None:
10296
def update_client_id(self):
10397
self._client_id = mqtt.base62(uuid.uuid4().int, padding=22)
10498

105-
def _async_check_keepalive(self) -> None:
106-
now = mqtt.time_func()
107-
# noinspection PyUnresolvedReferences
108-
if (
109-
now - self._last_disconnection > self._keepalive**2 # type: ignore[attr-defined]
110-
and now - self._last_device_msg_in > self._keepalive # type: ignore[attr-defined]
111-
):
112-
self._ping_t = self._last_device_msg_in
113-
11499
def _check_keepalive(self) -> None:
115-
self._async_check_keepalive()
116-
# noinspection PyUnresolvedReferences
100+
if not self.should_keepalive():
101+
self._ping_t = self.time_func() - KEEPALIVE
102+
# noinspection PyUnresolvedReferences
117103
super()._check_keepalive() # type: ignore[misc]
118104

119105
def sync_stop_loop(self) -> None:
@@ -142,7 +128,7 @@ def sync_connect(self) -> bool:
142128
if self._mqtt_port is None or self._mqtt_host is None:
143129
raise RoborockException("Mqtt information was not entered. Cannot connect.")
144130
_LOGGER.info("Connecting to mqtt")
145-
super().connect_async(host=self._mqtt_host, port=self._mqtt_port, keepalive=MQTT_KEEPALIVE)
131+
super().connect_async(host=self._mqtt_host, port=self._mqtt_port, keepalive=KEEPALIVE)
146132
return True
147133
return False
148134

roborock/exceptions.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
"""Roborock exceptions."""
22

33

4-
class RoborockException(BaseException):
4+
class RoborockException(Exception):
55
"""Class for Roborock exceptions."""
66

77

roborock/local_api.py

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,7 @@ def __init__(self, devices_info: Mapping[str, RoborockLocalDeviceInfo]):
2424
self.loop = get_running_loop_or_create_one()
2525
self.device_listener: dict[str, RoborockSocketListener] = {
2626
device_id: RoborockSocketListener(
27-
device_info.network_info.ip,
28-
device_info.device.local_key,
29-
self.on_message,
27+
device_info.network_info.ip, device_info.device.local_key, self.on_message, self.on_disconnect
3028
)
3129
for device_id, device_info in devices_info.items()
3230
}
@@ -83,6 +81,9 @@ async def send_message(self, device_id: str, roborock_messages: list[RoborockMes
8381
listener = self.device_listener.get(device_id)
8482
if listener is None:
8583
raise RoborockException(f"No device listener for {device_id}")
84+
if not self.should_keepalive():
85+
listener.disconnect()
86+
8687
_LOGGER.debug(f"Requesting device with {roborock_messages}")
8788
await listener.send_message(msg)
8889

@@ -113,12 +114,14 @@ def __init__(
113114
ip: str,
114115
local_key: str,
115116
on_message: Callable[[list[RoborockMessage]], None],
117+
on_disconnect: Callable[[Optional[Exception]], None],
116118
timeout: float | int = QUEUE_TIMEOUT,
117119
):
118120
self.ip = ip
119121
self.local_key = local_key
120122
self.loop = get_running_loop_or_create_one()
121123
self.on_message = on_message
124+
self.on_disconnect = on_disconnect
122125
self.timeout = timeout
123126
self.remaining = b""
124127
self.transport: Transport | None = None
@@ -132,8 +135,8 @@ def data_received(self, message):
132135
self.remaining = remaining
133136
self.on_message(parser_msg)
134137

135-
def connection_lost(self, exc):
136-
_LOGGER.debug("The server closed the connection")
138+
def connection_lost(self, exc: Optional[Exception]):
139+
self.on_disconnect(exc)
137140

138141
def is_connected(self):
139142
return self.transport and self.transport.is_reading()

0 commit comments

Comments
 (0)