From 2c42a319a27bf3b00f09702f447a307a4b89cce0 Mon Sep 17 00:00:00 2001 From: Luke Date: Mon, 24 Jul 2023 10:37:37 -0600 Subject: [PATCH] Add Fallback to cloud api for Roborock (#96147) Co-authored-by: Franck Nijhof --- homeassistant/components/roborock/__init__.py | 16 ++++++++-------- .../components/roborock/coordinator.py | 18 ++++++++++++++++++ homeassistant/components/roborock/device.py | 9 ++++----- homeassistant/components/roborock/switch.py | 14 +++++--------- 4 files changed, 35 insertions(+), 22 deletions(-) diff --git a/homeassistant/components/roborock/__init__.py b/homeassistant/components/roborock/__init__.py index 1a308f9dff9..b310b2bb2ba 100644 --- a/homeassistant/components/roborock/__init__.py +++ b/homeassistant/components/roborock/__init__.py @@ -36,24 +36,20 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: } product_info = {product.id: product for product in home_data.products} # Create a mqtt_client, which is needed to get the networking information of the device for local connection and in the future, get the map. - mqtt_clients = [ - RoborockMqttClient( + mqtt_clients = { + device.duid: RoborockMqttClient( user_data, DeviceData(device, product_info[device.product_id].model) ) for device in device_map.values() - ] + } network_results = await asyncio.gather( - *(mqtt_client.get_networking() for mqtt_client in mqtt_clients) + *(mqtt_client.get_networking() for mqtt_client in mqtt_clients.values()) ) network_info = { device.duid: result for device, result in zip(device_map.values(), network_results) if result is not None } - await asyncio.gather( - *(mqtt_client.async_disconnect() for mqtt_client in mqtt_clients), - return_exceptions=True, - ) if not network_info: raise ConfigEntryNotReady( "Could not get network information about your devices" @@ -65,7 +61,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: device, network_info[device_id], product_info[device.product_id], + mqtt_clients[device.duid], ) + await asyncio.gather( + *(coordinator.verify_api() for coordinator in coordinator_map.values()) + ) # If one device update fails - we still want to set up other devices await asyncio.gather( *( diff --git a/homeassistant/components/roborock/coordinator.py b/homeassistant/components/roborock/coordinator.py index ba9571a95f5..6ba6f3915ec 100644 --- a/homeassistant/components/roborock/coordinator.py +++ b/homeassistant/components/roborock/coordinator.py @@ -4,6 +4,7 @@ from __future__ import annotations from datetime import timedelta import logging +from roborock.cloud_api import RoborockMqttClient from roborock.containers import DeviceData, HomeDataDevice, HomeDataProduct, NetworkInfo from roborock.exceptions import RoborockException from roborock.local_api import RoborockLocalClient @@ -30,6 +31,7 @@ class RoborockDataUpdateCoordinator(DataUpdateCoordinator[DeviceProp]): device: HomeDataDevice, device_networking: NetworkInfo, product_info: HomeDataProduct, + cloud_api: RoborockMqttClient | None = None, ) -> None: """Initialize.""" super().__init__(hass, _LOGGER, name=DOMAIN, update_interval=SCAN_INTERVAL) @@ -41,6 +43,7 @@ class RoborockDataUpdateCoordinator(DataUpdateCoordinator[DeviceProp]): ) device_data = DeviceData(device, product_info.model, device_networking.ip) self.api = RoborockLocalClient(device_data) + self.cloud_api = cloud_api self.device_info = DeviceInfo( name=self.roborock_device_info.device.name, identifiers={(DOMAIN, self.roborock_device_info.device.duid)}, @@ -49,6 +52,21 @@ class RoborockDataUpdateCoordinator(DataUpdateCoordinator[DeviceProp]): sw_version=self.roborock_device_info.device.fv, ) + async def verify_api(self) -> None: + """Verify that the api is reachable. If it is not, switch clients.""" + try: + await self.api.ping() + except RoborockException: + if isinstance(self.api, RoborockLocalClient): + _LOGGER.warning( + "Using the cloud API for device %s. This is not recommended as it can lead to rate limiting. We recommend making your vacuum accessible by your Home Assistant instance", + self.roborock_device_info.device.duid, + ) + # We use the cloud api if the local api fails to connect. + self.api = self.cloud_api + # Right now this should never be called if the cloud api is the primary api, + # but in the future if it is, a new else should be added. + async def release(self) -> None: """Disconnect from API.""" await self.api.async_disconnect() diff --git a/homeassistant/components/roborock/device.py b/homeassistant/components/roborock/device.py index 86d578d852a..c40e47ada99 100644 --- a/homeassistant/components/roborock/device.py +++ b/homeassistant/components/roborock/device.py @@ -2,11 +2,10 @@ from typing import Any -from roborock.api import AttributeCache +from roborock.api import AttributeCache, RoborockClient from roborock.command_cache import CacheableAttribute from roborock.containers import Status from roborock.exceptions import RoborockException -from roborock.local_api import RoborockLocalClient from roborock.roborock_typing import RoborockCommand from homeassistant.exceptions import HomeAssistantError @@ -22,7 +21,7 @@ class RoborockEntity(Entity): _attr_has_entity_name = True def __init__( - self, unique_id: str, device_info: DeviceInfo, api: RoborockLocalClient + self, unique_id: str, device_info: DeviceInfo, api: RoborockClient ) -> None: """Initialize the coordinated Roborock Device.""" self._attr_unique_id = unique_id @@ -30,8 +29,8 @@ class RoborockEntity(Entity): self._api = api @property - def api(self) -> RoborockLocalClient: - """Return the Api.""" + def api(self) -> RoborockClient: + """Returns the api.""" return self._api def get_cache(self, attribute: CacheableAttribute) -> AttributeCache: diff --git a/homeassistant/components/roborock/switch.py b/homeassistant/components/roborock/switch.py index a0b3d5be295..312753ced01 100644 --- a/homeassistant/components/roborock/switch.py +++ b/homeassistant/components/roborock/switch.py @@ -9,13 +9,11 @@ from typing import Any from roborock.api import AttributeCache from roborock.command_cache import CacheableAttribute -from roborock.local_api import RoborockLocalClient from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription from homeassistant.config_entries import ConfigEntry from homeassistant.const import EntityCategory from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util import slugify @@ -121,9 +119,8 @@ async def async_setup_entry( valid_entities.append( RoborockSwitch( f"{description.key}_{slugify(coordinator.roborock_device_info.device.duid)}", - coordinator.device_info, + coordinator, description, - coordinator.api, ) ) async_add_entities(valid_entities) @@ -137,13 +134,12 @@ class RoborockSwitch(RoborockEntity, SwitchEntity): def __init__( self, unique_id: str, - device_info: DeviceInfo, - description: RoborockSwitchDescription, - api: RoborockLocalClient, + coordinator: RoborockDataUpdateCoordinator, + entity_description: RoborockSwitchDescription, ) -> None: """Initialize the entity.""" - super().__init__(unique_id, device_info, api) - self.entity_description = description + self.entity_description = entity_description + super().__init__(unique_id, coordinator.device_info, coordinator.api) async def async_turn_off(self, **kwargs: Any) -> None: """Turn off the switch."""