Skip to content

Commit 06d051c

Browse files
allenporterCopilot
andauthored
fix: Add device logger (#663)
* 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 ``` * chore: Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * chore: use the existing device logger --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent 109d05b commit 06d051c

File tree

6 files changed

+79
-65
lines changed

6 files changed

+79
-65
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
@@ -14,6 +14,7 @@
1414
from roborock.data import HomeDataDevice, HomeDataProduct
1515
from roborock.exceptions import RoborockException
1616
from roborock.roborock_message import RoborockMessage
17+
from roborock.util import RoborockLoggerAdapter
1718

1819
from .channel import Channel
1920
from .traits import Trait
@@ -59,6 +60,7 @@ def __init__(
5960
"""
6061
TraitsMixin.__init__(self, trait)
6162
self._duid = device_info.duid
63+
self._logger = RoborockLoggerAdapter(self._duid, _LOGGER)
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 for device %s", 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 & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
from roborock.roborock_message import RoborockMessage, RoborockMessageProtocol
1212

1313
from ..protocols.v1_protocol import LocalProtocolVersion
14-
from ..util import get_next_int
14+
from ..util import RoborockLoggerAdapter, get_next_int
1515
from .channel import Channel
1616

1717
_LOGGER = logging.getLogger(__name__)
@@ -52,11 +52,12 @@ class LocalChannel(Channel):
5252
format most parsing to higher-level components.
5353
"""
5454

55-
def __init__(self, host: str, local_key: str):
55+
def __init__(self, host: str, local_key: str, device_uid: str) -> None:
5656
self._host = host
57+
self._logger = RoborockLoggerAdapter(device_uid, _LOGGER)
5758
self._transport: asyncio.Transport | None = None
5859
self._protocol: _LocalProtocol | None = None
59-
self._subscribers: CallbackList[RoborockMessage] = CallbackList(_LOGGER)
60+
self._subscribers: CallbackList[RoborockMessage] = CallbackList(self._logger)
6061
self._is_connected = False
6162
self._local_protocol_version: LocalProtocolVersion | None = None
6263
self._keep_alive_task: asyncio.Task[None] | None = None
@@ -80,11 +81,11 @@ def _update_encoder_decoder(self, params: LocalChannelParams) -> None:
8081
local_key=params.local_key, connect_nonce=params.connect_nonce, ack_nonce=params.ack_nonce
8182
)
8283
# Callback to decode messages and dispatch to subscribers
83-
self._dispatch = decoder_callback(self._decoder, self._subscribers, _LOGGER)
84+
self._dispatch = decoder_callback(self._decoder, self._subscribers, self._logger)
8485

8586
async def _do_hello(self, local_protocol_version: LocalProtocolVersion) -> LocalChannelParams | None:
8687
"""Perform the initial handshaking and return encoder params if successful."""
87-
_LOGGER.debug(
88+
self._logger.debug(
8889
"Attempting to use the %s protocol for client %s...",
8990
local_protocol_version,
9091
self._host,
@@ -101,7 +102,7 @@ async def _do_hello(self, local_protocol_version: LocalProtocolVersion) -> Local
101102
request_id=request.seq,
102103
response_protocol=RoborockMessageProtocol.HELLO_RESPONSE,
103104
)
104-
_LOGGER.debug(
105+
self._logger.debug(
105106
"Client %s speaks the %s protocol.",
106107
self._host,
107108
local_protocol_version,
@@ -110,7 +111,7 @@ async def _do_hello(self, local_protocol_version: LocalProtocolVersion) -> Local
110111
local_key=self._params.local_key, connect_nonce=self._params.connect_nonce, ack_nonce=response.random
111112
)
112113
except RoborockException as e:
113-
_LOGGER.debug(
114+
self._logger.debug(
114115
"Client %s did not respond or does not speak the %s protocol. %s",
115116
self._host,
116117
local_protocol_version,
@@ -153,7 +154,7 @@ async def _keep_alive_loop(self) -> None:
153154
except asyncio.CancelledError:
154155
break
155156
except Exception:
156-
_LOGGER.debug("Keep-alive ping failed", exc_info=True)
157+
self._logger.debug("Keep-alive ping failed", exc_info=True)
157158
# Retry next interval
158159

159160
@property
@@ -176,9 +177,9 @@ def is_local_connected(self) -> bool:
176177
async def connect(self) -> None:
177178
"""Connect to the device and negotiate protocol."""
178179
if self._is_connected:
179-
_LOGGER.debug("Unexpected call to connect when already connected")
180+
self._logger.debug("Unexpected call to connect when already connected")
180181
return
181-
_LOGGER.debug("Connecting to %s:%s", self._host, _PORT)
182+
self._logger.debug("Connecting to %s:%s", self._host, _PORT)
182183
loop = asyncio.get_running_loop()
183184
protocol = _LocalProtocol(self._data_received, self._connection_lost)
184185
try:
@@ -208,13 +209,13 @@ def close(self) -> None:
208209
if self._transport:
209210
self._transport.close()
210211
else:
211-
_LOGGER.warning("Close called but transport is already None")
212+
self._logger.warning("Close called but transport is already None")
212213
self._transport = None
213214
self._is_connected = False
214215

215216
def _connection_lost(self, exc: Exception | None) -> None:
216217
"""Handle connection loss."""
217-
_LOGGER.debug("Connection lost to %s", self._host, exc_info=exc)
218+
self._logger.debug("Connection lost to %s", self._host, exc_info=exc)
218219
if self._keep_alive_task:
219220
self._keep_alive_task.cancel()
220221
self._keep_alive_task = None
@@ -236,12 +237,12 @@ async def publish(self, message: RoborockMessage) -> None:
236237
try:
237238
encoded_msg = self._encoder(message)
238239
except Exception as err:
239-
_LOGGER.exception("Error encoding MQTT message: %s", err)
240+
self._logger.exception("Error encoding MQTT message: %s", err)
240241
raise RoborockException(f"Failed to encode MQTT message: {err}") from err
241242
try:
242243
self._transport.write(encoded_msg)
243244
except Exception as err:
244-
logging.exception("Uncaught error sending command")
245+
self._logger.exception("Uncaught error sending command")
245246
raise RoborockException(f"Failed to send message: {message}") from err
246247

247248
async def _send_message(
@@ -276,7 +277,7 @@ def find_response(response_message: RoborockMessage) -> None:
276277
LocalSession = Callable[[str], LocalChannel]
277278

278279

279-
def create_local_session(local_key: str) -> LocalSession:
280+
def create_local_session(local_key: str, device_uid: str) -> LocalSession:
280281
"""Creates a local session which can create local channels.
281282
282283
This plays a role similar to the MqttSession but is really just a factory
@@ -285,6 +286,6 @@ def create_local_session(local_key: str) -> LocalSession:
285286

286287
def create_local_channel(host: str) -> LocalChannel:
287288
"""Create a LocalChannel instance for the given host."""
288-
return LocalChannel(host, local_key)
289+
return LocalChannel(host, local_key, device_uid)
289290

290291
return create_local_channel

roborock/devices/mqtt_channel.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from roborock.mqtt.session import MqttParams, MqttSession, MqttSessionException
1010
from roborock.protocol import create_mqtt_decoder, create_mqtt_encoder
1111
from roborock.roborock_message import RoborockMessage
12+
from roborock.util import RoborockLoggerAdapter
1213

1314
from .channel import Channel
1415

@@ -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 = RoborockLoggerAdapter(duid, _LOGGER)
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)