Skip to content

Commit 11816e4

Browse files
committed
fix: Add device logger
This prefixes all log lines for the device with a standard log prefix. Example: ``` DEBUG:roborock.devices.v1_channel:abcdefghblachfdfs Background reconnect task cancelled DEBUG:roborock.devices.v1_channel:zzyyaaqwwerrefdsa Background reconnect task cancelled DEBUG:roborock.devices.local_channel:abcdefghblachfdfs Connection lost to 192.168.1.11 DEBUG:roborock.devices.local_channel:zzyyaaqwwerrefdsa Connection lost to 192.168.1.10 ```
1 parent f13e87a commit 11816e4

File tree

7 files changed

+97
-64
lines changed

7 files changed

+97
-64
lines changed

roborock/callbacks.py

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,9 @@
1010
V = TypeVar("V")
1111

1212

13-
def safe_callback(callback: Callable[[V], None], logger: logging.Logger | None = None) -> Callable[[V], None]:
13+
def safe_callback(
14+
callback: Callable[[V], None], logger: logging.Logger | logging.LoggerAdapter | None = None
15+
) -> Callable[[V], None]:
1416
"""Wrap a callback to catch and log exceptions.
1517
1618
This is useful for ensuring that errors in callbacks do not propagate
@@ -36,7 +38,7 @@ class CallbackMap(Generic[K, V]):
3638
when a value is received for a specific key.
3739
"""
3840

39-
def __init__(self, logger: logging.Logger | None = None) -> None:
41+
def __init__(self, logger: logging.Logger | logging.LoggerAdapter | None = None) -> None:
4042
self._callbacks: dict[K, list[Callable[[V], None]]] = {}
4143
self._logger = logger or _LOGGER
4244

@@ -79,7 +81,7 @@ class CallbackList(Generic[V]):
7981
additional callbacks to the list at any time.
8082
"""
8183

82-
def __init__(self, logger: logging.Logger | None = None) -> None:
84+
def __init__(self, logger: logging.Logger | logging.LoggerAdapter | None = None) -> None:
8385
self._callbacks: list[Callable[[V], None]] = []
8486
self._logger = logger or _LOGGER
8587

@@ -101,7 +103,9 @@ def __call__(self, value: V) -> None:
101103

102104

103105
def decoder_callback(
104-
decoder: Callable[[K], list[V]], callback: Callable[[V], None], logger: logging.Logger | None = None
106+
decoder: Callable[[K], list[V]],
107+
callback: Callable[[V], None],
108+
logger: logging.Logger | logging.LoggerAdapter | None = None,
105109
) -> Callable[[K], None]:
106110
"""Create a callback that decodes messages using a decoder and invokes a callback.
107111
@@ -120,7 +124,7 @@ def wrapper(data: K) -> None:
120124
logger.warning("Failed to decode message: %s", data)
121125
return
122126
for message in messages:
123-
_LOGGER.debug("Decoded message: %s", message)
127+
logger.debug("Decoded message: %s", message)
124128
safe_cb(message)
125129

126130
return wrapper

roborock/devices/device.py

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from typing import Any, TypeVar, cast
1313

1414
from roborock.data import HomeDataDevice, HomeDataProduct
15+
from roborock.devices.logger import DeviceLoggerAdapter
1516
from roborock.exceptions import RoborockException
1617
from roborock.roborock_message import RoborockMessage
1718

@@ -59,6 +60,7 @@ def __init__(
5960
"""
6061
TraitsMixin.__init__(self, trait)
6162
self._duid = device_info.duid
63+
self._logger = DeviceLoggerAdapter(_LOGGER, self._duid)
6264
self._name = device_info.name
6365
self._device_info = device_info
6466
self._product = product
@@ -134,14 +136,11 @@ async def connect_loop() -> None:
134136
return
135137
except RoborockException as e:
136138
start_attempt.set()
137-
_LOGGER.info("Failed to connect to device %s: %s", self.name, e)
138-
_LOGGER.info(
139-
"Retrying connection to device %s in %s seconds", self.name, backoff.total_seconds()
140-
)
139+
self._logger.info("Failed to connect (retry %s): %s", backoff.total_seconds(), e)
141140
await asyncio.sleep(backoff.total_seconds())
142141
backoff = min(backoff * BACKOFF_MULTIPLIER, MAX_BACKOFF_INTERVAL)
143142
except asyncio.CancelledError:
144-
_LOGGER.info("connect_loop for device %s was cancelled", self.name)
143+
self._logger.debug("connect_loop was cancelled", self.duid)
145144
# Clean exit on cancellation
146145
return
147146
finally:
@@ -152,14 +151,14 @@ async def connect_loop() -> None:
152151
try:
153152
await asyncio.wait_for(start_attempt.wait(), timeout=START_ATTEMPT_TIMEOUT.total_seconds())
154153
except TimeoutError:
155-
_LOGGER.debug("Initial connection attempt to device %s is taking longer than expected", self.name)
154+
self._logger.debug("Initial connection attempt took longer than expected, will keep trying in background")
156155

157156
async def connect(self) -> None:
158157
"""Connect to the device using the appropriate protocol channel."""
159158
if self._unsub:
160159
raise ValueError("Already connected to the device")
161160
unsub = await self._channel.subscribe(self._on_message)
162-
_LOGGER.info("Connected to V1 device %s", self.name)
161+
self._logger.info("Connecting to device")
163162
if self.v1_properties is not None:
164163
try:
165164
await self.v1_properties.discover_features()
@@ -182,7 +181,7 @@ async def close(self) -> None:
182181

183182
def _on_message(self, message: RoborockMessage) -> None:
184183
"""Handle incoming messages from the device."""
185-
_LOGGER.debug("Received message from device: %s", message)
184+
self._logger.debug("Received message from device: %s", message)
186185

187186
def diagnostic_data(self) -> dict[str, Any]:
188187
"""Return diagnostics information about the device."""

roborock/devices/local_channel.py

Lines changed: 17 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
from ..protocols.v1_protocol import LocalProtocolVersion
1414
from ..util import get_next_int
1515
from .channel import Channel
16+
from .logger import DeviceLoggerAdapter
1617

1718
_LOGGER = logging.getLogger(__name__)
1819
_PORT = 58867
@@ -52,11 +53,12 @@ class LocalChannel(Channel):
5253
format most parsing to higher-level components.
5354
"""
5455

55-
def __init__(self, host: str, local_key: str):
56+
def __init__(self, host: str, local_key: str, device_uid: str) -> None:
5657
self._host = host
58+
self._logger = DeviceLoggerAdapter(_LOGGER, device_uid)
5759
self._transport: asyncio.Transport | None = None
5860
self._protocol: _LocalProtocol | None = None
59-
self._subscribers: CallbackList[RoborockMessage] = CallbackList(_LOGGER)
61+
self._subscribers: CallbackList[RoborockMessage] = CallbackList(self._logger)
6062
self._is_connected = False
6163
self._local_protocol_version: LocalProtocolVersion | None = None
6264
self._keep_alive_task: asyncio.Task[None] | None = None
@@ -80,11 +82,11 @@ def _update_encoder_decoder(self, params: LocalChannelParams) -> None:
8082
local_key=params.local_key, connect_nonce=params.connect_nonce, ack_nonce=params.ack_nonce
8183
)
8284
# Callback to decode messages and dispatch to subscribers
83-
self._dispatch = decoder_callback(self._decoder, self._subscribers, _LOGGER)
85+
self._dispatch = decoder_callback(self._decoder, self._subscribers, self._logger)
8486

8587
async def _do_hello(self, local_protocol_version: LocalProtocolVersion) -> LocalChannelParams | None:
8688
"""Perform the initial handshaking and return encoder params if successful."""
87-
_LOGGER.debug(
89+
self._logger.debug(
8890
"Attempting to use the %s protocol for client %s...",
8991
local_protocol_version,
9092
self._host,
@@ -101,7 +103,7 @@ async def _do_hello(self, local_protocol_version: LocalProtocolVersion) -> Local
101103
request_id=request.seq,
102104
response_protocol=RoborockMessageProtocol.HELLO_RESPONSE,
103105
)
104-
_LOGGER.debug(
106+
self._logger.debug(
105107
"Client %s speaks the %s protocol.",
106108
self._host,
107109
local_protocol_version,
@@ -110,7 +112,7 @@ async def _do_hello(self, local_protocol_version: LocalProtocolVersion) -> Local
110112
local_key=self._params.local_key, connect_nonce=self._params.connect_nonce, ack_nonce=response.random
111113
)
112114
except RoborockException as e:
113-
_LOGGER.debug(
115+
self._logger.debug(
114116
"Client %s did not respond or does not speak the %s protocol. %s",
115117
self._host,
116118
local_protocol_version,
@@ -153,7 +155,7 @@ async def _keep_alive_loop(self) -> None:
153155
except asyncio.CancelledError:
154156
break
155157
except Exception:
156-
_LOGGER.debug("Keep-alive ping failed", exc_info=True)
158+
self._logger.debug("Keep-alive ping failed", exc_info=True)
157159
# Retry next interval
158160

159161
@property
@@ -176,9 +178,9 @@ def is_local_connected(self) -> bool:
176178
async def connect(self) -> None:
177179
"""Connect to the device and negotiate protocol."""
178180
if self._is_connected:
179-
_LOGGER.debug("Unexpected call to connect when already connected")
181+
self._logger.debug("Unexpected call to connect when already connected")
180182
return
181-
_LOGGER.debug("Connecting to %s:%s", self._host, _PORT)
183+
self._logger.debug("Connecting to %s:%s", self._host, _PORT)
182184
loop = asyncio.get_running_loop()
183185
protocol = _LocalProtocol(self._data_received, self._connection_lost)
184186
try:
@@ -208,13 +210,13 @@ def close(self) -> None:
208210
if self._transport:
209211
self._transport.close()
210212
else:
211-
_LOGGER.warning("Close called but transport is already None")
213+
self._logger.warning("Close called but transport is already None")
212214
self._transport = None
213215
self._is_connected = False
214216

215217
def _connection_lost(self, exc: Exception | None) -> None:
216218
"""Handle connection loss."""
217-
_LOGGER.debug("Connection lost to %s", self._host, exc_info=exc)
219+
self._logger.debug("Connection lost to %s", self._host, exc_info=exc)
218220
if self._keep_alive_task:
219221
self._keep_alive_task.cancel()
220222
self._keep_alive_task = None
@@ -236,12 +238,12 @@ async def publish(self, message: RoborockMessage) -> None:
236238
try:
237239
encoded_msg = self._encoder(message)
238240
except Exception as err:
239-
_LOGGER.exception("Error encoding MQTT message: %s", err)
241+
self._logger.exception("Error encoding MQTT message: %s", err)
240242
raise RoborockException(f"Failed to encode MQTT message: {err}") from err
241243
try:
242244
self._transport.write(encoded_msg)
243245
except Exception as err:
244-
logging.exception("Uncaught error sending command")
246+
self._logger.exception("Uncaught error sending command")
245247
raise RoborockException(f"Failed to send message: {message}") from err
246248

247249
async def _send_message(
@@ -276,7 +278,7 @@ def find_response(response_message: RoborockMessage) -> None:
276278
LocalSession = Callable[[str], LocalChannel]
277279

278280

279-
def create_local_session(local_key: str) -> LocalSession:
281+
def create_local_session(local_key: str, device_uid: str) -> LocalSession:
280282
"""Creates a local session which can create local channels.
281283
282284
This plays a role similar to the MqttSession but is really just a factory
@@ -285,6 +287,6 @@ def create_local_session(local_key: str) -> LocalSession:
285287

286288
def create_local_channel(host: str) -> LocalChannel:
287289
"""Create a LocalChannel instance for the given host."""
288-
return LocalChannel(host, local_key)
290+
return LocalChannel(host, local_key, device_uid)
289291

290292
return create_local_channel

roborock/devices/logger.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
"""Logger adapter for device-specific logging."""
2+
3+
import logging
4+
from collections.abc import MutableMapping
5+
from typing import Any
6+
7+
8+
class DeviceLoggerAdapter(logging.LoggerAdapter):
9+
"""A LoggerAdapter that prepends a [DUID] prefix to all messages."""
10+
11+
def __init__(self, logger: logging.Logger, duid: str):
12+
# The 'extra' dictionary can hold any custom context you need.
13+
# We define a custom 'prefix' key here.
14+
super().__init__(logger, {"prefix": duid})
15+
16+
def process(self, msg: str, kwargs: MutableMapping[str, Any]) -> tuple[str, MutableMapping[str, Any]]:
17+
"""Format the message by adding the device prefix."""
18+
return f"{self.extra['prefix']} {msg}", kwargs # type: ignore[index]

roborock/devices/mqtt_channel.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from roborock.roborock_message import RoborockMessage
1212

1313
from .channel import Channel
14+
from .logger import DeviceLoggerAdapter
1415

1516
_LOGGER = logging.getLogger(__name__)
1617

@@ -25,6 +26,7 @@ class MqttChannel(Channel):
2526
def __init__(self, mqtt_session: MqttSession, duid: str, local_key: str, rriot: RRiot, mqtt_params: MqttParams):
2627
self._mqtt_session = mqtt_session
2728
self._duid = duid
29+
self._logger = DeviceLoggerAdapter(_LOGGER, duid)
2830
self._local_key = local_key
2931
self._rriot = rriot
3032
self._mqtt_params = mqtt_params
@@ -74,12 +76,12 @@ async def publish(self, message: RoborockMessage) -> None:
7476
try:
7577
encoded_msg = self._encoder(message)
7678
except Exception as e:
77-
_LOGGER.exception("Error encoding MQTT message: %s", e)
79+
self._logger.exception("Error encoding MQTT message: %s", e)
7880
raise RoborockException(f"Failed to encode MQTT message: {e}") from e
7981
try:
8082
return await self._mqtt_session.publish(self._publish_topic, encoded_msg)
8183
except MqttSessionException as e:
82-
_LOGGER.exception("Error publishing MQTT message: %s", e)
84+
self._logger.exception("Error publishing MQTT message: %s", e)
8385
raise RoborockException(f"Failed to publish MQTT message: {e}") from e
8486

8587
async def restart(self) -> None:

0 commit comments

Comments
 (0)