Skip to content

Commit 80d7d5a

Browse files
allenporterCopilot
andauthored
fix: fallback to the cached network information on failure (#606)
* fix: fallback to the cached network information when failing to lookup network info * chore: Update roborock/devices/v1_channel.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent 27c61f9 commit 80d7d5a

File tree

2 files changed

+49
-9
lines changed

2 files changed

+49
-9
lines changed

roborock/devices/v1_channel.py

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,7 @@ async def subscribe(self, callback: Callable[[RoborockMessage], None]) -> Callab
142142
# Make an initial, optimistic attempt to connect to local with the
143143
# cache. The cache information will be refreshed by the background task.
144144
try:
145-
await self._local_connect(use_cache=True)
145+
await self._local_connect(prefer_cache=True)
146146
except RoborockException as err:
147147
_LOGGER.warning("Could not establish local connection for device %s: %s", self._device_uid, err)
148148

@@ -175,33 +175,37 @@ def unsub() -> None:
175175
self._callback = callback
176176
return unsub
177177

178-
async def _get_networking_info(self, *, use_cache: bool = True) -> NetworkInfo:
178+
async def _get_networking_info(self, *, prefer_cache: bool = True) -> NetworkInfo:
179179
"""Retrieve networking information for the device.
180180
181181
This is a cloud only command used to get the local device's IP address.
182182
"""
183183
cache_data = await self._cache.get()
184-
if use_cache and cache_data.network_info and (network_info := cache_data.network_info.get(self._device_uid)):
184+
if prefer_cache and cache_data.network_info and (network_info := cache_data.network_info.get(self._device_uid)):
185185
_LOGGER.debug("Using cached network info for device %s", self._device_uid)
186186
return network_info
187187
try:
188188
network_info = await self._mqtt_rpc_channel.send_command(
189189
RoborockCommand.GET_NETWORK_INFO, response_type=NetworkInfo
190190
)
191191
except RoborockException as e:
192+
_LOGGER.debug("Error fetching network info for device %s", self._device_uid)
193+
if cache_data.network_info and (network_info := cache_data.network_info.get(self._device_uid)):
194+
_LOGGER.debug("Falling back to cached network info for device %s after error", self._device_uid)
195+
return network_info
192196
raise RoborockException(f"Network info failed for device {self._device_uid}") from e
193197
_LOGGER.debug("Network info for device %s: %s", self._device_uid, network_info)
194198
self._last_network_info_refresh = datetime.datetime.now(datetime.UTC)
195199
cache_data.network_info[self._device_uid] = network_info
196200
await self._cache.set(cache_data)
197201
return network_info
198202

199-
async def _local_connect(self, *, use_cache: bool = True) -> None:
203+
async def _local_connect(self, *, prefer_cache: bool = True) -> None:
200204
"""Set up local connection if possible."""
201205
_LOGGER.debug(
202-
"Attempting to connect to local channel for device %s (use_cache=%s)", self._device_uid, use_cache
206+
"Attempting to connect to local channel for device %s (prefer_cache=%s)", self._device_uid, prefer_cache
203207
)
204-
networking_info = await self._get_networking_info(use_cache=use_cache)
208+
networking_info = await self._get_networking_info(prefer_cache=prefer_cache)
205209
host = networking_info.ip
206210
_LOGGER.debug("Connecting to local channel at %s", host)
207211
# Create a new local channel and connect
@@ -236,7 +240,7 @@ async def _background_reconnect(self) -> None:
236240
reconnect_backoff = min(reconnect_backoff * RECONNECT_MULTIPLIER, MAX_RECONNECT_INTERVAL)
237241

238242
use_cache = self._should_use_cache(local_connect_failures)
239-
await self._local_connect(use_cache=use_cache)
243+
await self._local_connect(prefer_cache=use_cache)
240244
# Reset backoff and failures on success
241245
reconnect_backoff = MIN_RECONNECT_INTERVAL
242246
local_connect_failures = 0

tests/devices/test_v1_channel.py

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@
3333
TEST_DEVICE_UID = "abc123"
3434
TEST_LOCAL_KEY = "local_key"
3535
TEST_SECURITY_DATA = SecurityData(endpoint="test_endpoint", nonce=b"test_nonce_16byte")
36-
TEST_HOST = "1.1.1.1"
36+
TEST_HOST = mock_data.TEST_LOCAL_API_HOST
3737

3838

3939
# Test messages for V1 protocol
@@ -112,19 +112,26 @@ def setup_mock_map_decoder() -> Iterator[Mock]:
112112
yield mock_create_decoder
113113

114114

115+
@pytest.fixture(name="cache")
116+
def cache_fixtures() -> InMemoryCache:
117+
"""Mock cache for testing."""
118+
return InMemoryCache()
119+
120+
115121
@pytest.fixture(name="v1_channel")
116122
def setup_v1_channel(
117123
mock_mqtt_channel: Mock,
118124
mock_local_session: Mock,
119125
mock_create_map_response_decoder: Mock,
126+
cache: InMemoryCache,
120127
) -> V1Channel:
121128
"""Fixture to set up the V1 channel for tests."""
122129
return V1Channel(
123130
device_uid=TEST_DEVICE_UID,
124131
security_data=TEST_SECURITY_DATA,
125132
mqtt_channel=mock_mqtt_channel,
126133
local_session=mock_local_session,
127-
cache=InMemoryCache(),
134+
cache=cache,
128135
)
129136

130137

@@ -437,6 +444,35 @@ async def test_v1_channel_local_connect_network_info_failure(
437444
await v1_channel._local_connect()
438445

439446

447+
async def test_v1_channel_local_connect_network_info_failure_fallback_to_cache(
448+
mock_mqtt_channel: FakeChannel,
449+
mock_local_session: Mock,
450+
v1_channel: V1Channel,
451+
cache: InMemoryCache,
452+
) -> None:
453+
"""Test local connection falls back to cache when network info retrieval fails."""
454+
# Create a cache with pre-populated network info
455+
cache_data = CacheData()
456+
cache_data.network_info[TEST_DEVICE_UID] = TEST_NETWORKING_INFO
457+
await cache.set(cache_data)
458+
459+
# Setup: MQTT fails to publish
460+
mock_mqtt_channel.publish_side_effect = RoborockException("Network info failed")
461+
462+
# Attempt local connect, forcing a refresh (prefer_cache=False)
463+
# This should try MQTT, fail, and then fall back to cache
464+
await v1_channel._local_connect(prefer_cache=False)
465+
466+
# Verify local connection was established
467+
assert v1_channel.is_local_connected
468+
469+
# Verify MQTT was attempted (published message)
470+
assert mock_mqtt_channel.published_messages
471+
472+
# Verify local session was created with the correct IP from cache
473+
mock_local_session.assert_called_once_with(TEST_HOST)
474+
475+
440476
async def test_v1_channel_command_encoding_validation(
441477
v1_channel: V1Channel,
442478
mock_mqtt_channel: Mock,

0 commit comments

Comments
 (0)