Add Fallback to cloud api for Roborock (#96147)

Co-authored-by: Franck Nijhof <frenck@frenck.nl>
This commit is contained in:
Luke 2023-07-24 10:37:37 -06:00 committed by GitHub
parent 6b980eb0a7
commit 2c42a319a2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 35 additions and 22 deletions

View File

@ -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} 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. # 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 = [ mqtt_clients = {
RoborockMqttClient( device.duid: RoborockMqttClient(
user_data, DeviceData(device, product_info[device.product_id].model) user_data, DeviceData(device, product_info[device.product_id].model)
) )
for device in device_map.values() for device in device_map.values()
] }
network_results = await asyncio.gather( 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 = { network_info = {
device.duid: result device.duid: result
for device, result in zip(device_map.values(), network_results) for device, result in zip(device_map.values(), network_results)
if result is not None 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: if not network_info:
raise ConfigEntryNotReady( raise ConfigEntryNotReady(
"Could not get network information about your devices" "Could not get network information about your devices"
@ -65,6 +61,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
device, device,
network_info[device_id], network_info[device_id],
product_info[device.product_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 # If one device update fails - we still want to set up other devices
await asyncio.gather( await asyncio.gather(

View File

@ -4,6 +4,7 @@ from __future__ import annotations
from datetime import timedelta from datetime import timedelta
import logging import logging
from roborock.cloud_api import RoborockMqttClient
from roborock.containers import DeviceData, HomeDataDevice, HomeDataProduct, NetworkInfo from roborock.containers import DeviceData, HomeDataDevice, HomeDataProduct, NetworkInfo
from roborock.exceptions import RoborockException from roborock.exceptions import RoborockException
from roborock.local_api import RoborockLocalClient from roborock.local_api import RoborockLocalClient
@ -30,6 +31,7 @@ class RoborockDataUpdateCoordinator(DataUpdateCoordinator[DeviceProp]):
device: HomeDataDevice, device: HomeDataDevice,
device_networking: NetworkInfo, device_networking: NetworkInfo,
product_info: HomeDataProduct, product_info: HomeDataProduct,
cloud_api: RoborockMqttClient | None = None,
) -> None: ) -> None:
"""Initialize.""" """Initialize."""
super().__init__(hass, _LOGGER, name=DOMAIN, update_interval=SCAN_INTERVAL) 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) device_data = DeviceData(device, product_info.model, device_networking.ip)
self.api = RoborockLocalClient(device_data) self.api = RoborockLocalClient(device_data)
self.cloud_api = cloud_api
self.device_info = DeviceInfo( self.device_info = DeviceInfo(
name=self.roborock_device_info.device.name, name=self.roborock_device_info.device.name,
identifiers={(DOMAIN, self.roborock_device_info.device.duid)}, identifiers={(DOMAIN, self.roborock_device_info.device.duid)},
@ -49,6 +52,21 @@ class RoborockDataUpdateCoordinator(DataUpdateCoordinator[DeviceProp]):
sw_version=self.roborock_device_info.device.fv, 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: async def release(self) -> None:
"""Disconnect from API.""" """Disconnect from API."""
await self.api.async_disconnect() await self.api.async_disconnect()

View File

@ -2,11 +2,10 @@
from typing import Any from typing import Any
from roborock.api import AttributeCache from roborock.api import AttributeCache, RoborockClient
from roborock.command_cache import CacheableAttribute from roborock.command_cache import CacheableAttribute
from roborock.containers import Status from roborock.containers import Status
from roborock.exceptions import RoborockException from roborock.exceptions import RoborockException
from roborock.local_api import RoborockLocalClient
from roborock.roborock_typing import RoborockCommand from roborock.roborock_typing import RoborockCommand
from homeassistant.exceptions import HomeAssistantError from homeassistant.exceptions import HomeAssistantError
@ -22,7 +21,7 @@ class RoborockEntity(Entity):
_attr_has_entity_name = True _attr_has_entity_name = True
def __init__( def __init__(
self, unique_id: str, device_info: DeviceInfo, api: RoborockLocalClient self, unique_id: str, device_info: DeviceInfo, api: RoborockClient
) -> None: ) -> None:
"""Initialize the coordinated Roborock Device.""" """Initialize the coordinated Roborock Device."""
self._attr_unique_id = unique_id self._attr_unique_id = unique_id
@ -30,8 +29,8 @@ class RoborockEntity(Entity):
self._api = api self._api = api
@property @property
def api(self) -> RoborockLocalClient: def api(self) -> RoborockClient:
"""Return the Api.""" """Returns the api."""
return self._api return self._api
def get_cache(self, attribute: CacheableAttribute) -> AttributeCache: def get_cache(self, attribute: CacheableAttribute) -> AttributeCache:

View File

@ -9,13 +9,11 @@ from typing import Any
from roborock.api import AttributeCache from roborock.api import AttributeCache
from roborock.command_cache import CacheableAttribute from roborock.command_cache import CacheableAttribute
from roborock.local_api import RoborockLocalClient
from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import EntityCategory from homeassistant.const import EntityCategory
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.util import slugify from homeassistant.util import slugify
@ -121,9 +119,8 @@ async def async_setup_entry(
valid_entities.append( valid_entities.append(
RoborockSwitch( RoborockSwitch(
f"{description.key}_{slugify(coordinator.roborock_device_info.device.duid)}", f"{description.key}_{slugify(coordinator.roborock_device_info.device.duid)}",
coordinator.device_info, coordinator,
description, description,
coordinator.api,
) )
) )
async_add_entities(valid_entities) async_add_entities(valid_entities)
@ -137,13 +134,12 @@ class RoborockSwitch(RoborockEntity, SwitchEntity):
def __init__( def __init__(
self, self,
unique_id: str, unique_id: str,
device_info: DeviceInfo, coordinator: RoborockDataUpdateCoordinator,
description: RoborockSwitchDescription, entity_description: RoborockSwitchDescription,
api: RoborockLocalClient,
) -> None: ) -> None:
"""Initialize the entity.""" """Initialize the entity."""
super().__init__(unique_id, device_info, api) self.entity_description = entity_description
self.entity_description = description super().__init__(unique_id, coordinator.device_info, coordinator.api)
async def async_turn_off(self, **kwargs: Any) -> None: async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn off the switch.""" """Turn off the switch."""