Skip to content

Commit 4ad9bcd

Browse files
authored
fix: Throw MQTT authentication errors as authentication related exceptions (#634)
* fix: Throw MQTT authentication errors as authentication related exceptions The intent is to allow callers to catch this to know when they need to re-authenticate a device and obtain new mqtt params. * fix: Update the exception handling behavior to account for ambiguity
1 parent b305b5b commit 4ad9bcd

File tree

3 files changed

+70
-4
lines changed

3 files changed

+70
-4
lines changed

roborock/mqtt/roborock_session.py

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,11 @@
1515
from contextlib import asynccontextmanager
1616

1717
import aiomqtt
18-
from aiomqtt import MqttError, TLSParameters
18+
from aiomqtt import MqttCodeError, MqttError, TLSParameters
1919

2020
from roborock.callbacks import CallbackMap
2121

22-
from .session import MqttParams, MqttSession, MqttSessionException
22+
from .session import MqttParams, MqttSession, MqttSessionException, MqttSessionUnauthorized
2323

2424
_LOGGER = logging.getLogger(__name__)
2525
_MQTT_LOGGER = logging.getLogger(f"{__name__}.aiomqtt")
@@ -33,6 +33,16 @@
3333
BACKOFF_MULTIPLIER = 1.5
3434

3535

36+
class MqttReasonCode:
37+
"""MQTT Reason Codes used by Roborock devices.
38+
39+
This is a subset of paho.mqtt.reasoncodes.ReasonCode where we would like
40+
different error handling behavior.
41+
"""
42+
43+
RC_ERROR_UNAUTHORIZED = 135
44+
45+
3646
class RoborockMqttSession(MqttSession):
3747
"""An MQTT session for sending and receiving messages.
3848
@@ -83,6 +93,10 @@ async def start(self) -> None:
8393
self._reconnect_task = loop.create_task(self._run_reconnect_loop(start_future))
8494
try:
8595
await start_future
96+
except MqttCodeError as err:
97+
if err.rc == MqttReasonCode.RC_ERROR_UNAUTHORIZED:
98+
raise MqttSessionUnauthorized(f"Authorization error starting MQTT session: {err}") from err
99+
raise MqttSessionException(f"Error starting MQTT session: {err}") from err
86100
except MqttError as err:
87101
raise MqttSessionException(f"Error starting MQTT session: {err}") from err
88102
except Exception as err:

roborock/mqtt/session.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,4 +64,17 @@ async def close(self) -> None:
6464

6565

6666
class MqttSessionException(RoborockException):
67-
""" "Raised when there is an error communicating with MQTT."""
67+
"""Raised when there is an error communicating with MQTT."""
68+
69+
70+
class MqttSessionUnauthorized(RoborockException):
71+
"""Raised when there is an authorization error communicating with MQTT.
72+
73+
This error may be raised in multiple scenarios so there is not a well
74+
defined behavior for how the caller should behave. The two cases are:
75+
- Rate limiting is in effect and the caller should retry after some time.
76+
- The credentials are invalid and the caller needs to obtain new credentials
77+
78+
However, it is observed that obtaining new credentials may resolve the
79+
issue in both cases.
80+
"""

tests/mqtt/test_roborock_session.py

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
import pytest
1313

1414
from roborock.mqtt.roborock_session import RoborockMqttSession, create_mqtt_session
15-
from roborock.mqtt.session import MqttParams, MqttSessionException
15+
from roborock.mqtt.session import MqttParams, MqttSessionException, MqttSessionUnauthorized
1616
from tests import mqtt_packet
1717
from tests.conftest import FakeSocketHandler
1818

@@ -366,3 +366,42 @@ async def test_idle_timeout_multiple_callbacks(mock_mqtt_client: AsyncMock) -> N
366366
mock_mqtt_client.unsubscribe.assert_called_once_with(topic)
367367

368368
await session.close()
369+
370+
371+
@pytest.mark.parametrize(
372+
("side_effect", "expected_exception", "match"),
373+
[
374+
(
375+
aiomqtt.MqttError("Connection failed"),
376+
MqttSessionException,
377+
"Error starting MQTT session",
378+
),
379+
(
380+
aiomqtt.MqttCodeError(rc=135),
381+
MqttSessionUnauthorized,
382+
"Authorization error starting MQTT session",
383+
),
384+
(
385+
aiomqtt.MqttCodeError(rc=128),
386+
MqttSessionException,
387+
"Error starting MQTT session",
388+
),
389+
(
390+
ValueError("Unexpected"),
391+
MqttSessionException,
392+
"Unexpected error starting session",
393+
),
394+
],
395+
)
396+
async def test_connect_failure(
397+
side_effect: Exception,
398+
expected_exception: type[Exception],
399+
match: str,
400+
) -> None:
401+
"""Test connection failure with different exceptions."""
402+
mock_aenter = AsyncMock()
403+
mock_aenter.side_effect = side_effect
404+
405+
with patch("roborock.mqtt.roborock_session.aiomqtt.Client.__aenter__", mock_aenter):
406+
with pytest.raises(expected_exception, match=match):
407+
await create_mqtt_session(FAKE_PARAMS)

0 commit comments

Comments
 (0)