-
-
Notifications
You must be signed in to change notification settings - Fork 38.3k
Overhaul Roborock integration to use new devices based API #154837
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
23 commits
Select commit
Hold shift + click to select a range
ec2773b
First pass at roborock rewrite
allenporter 78f4199
Update tests after forwarding to head
allenporter 8094714
Update to use newer APIs
allenporter 04ee238
Use map content from the home trait
allenporter 6c99c3e
Change formatting to reduce review diffs
allenporter 46f717d
Reduce coordinator diffs
allenporter edcfd6e
Update refresh behavior
allenporter 4dbc287
Further reduce diffs for update timestamp
allenporter 910453e
Add back support for flagging repair issues for local connection issues
allenporter ebad652
Reduce diffs for improved readability
allenporter 8d8b136
Remove unnecessary logger
allenporter cc300af
Add comment for reset consumable error handling translation improvements
allenporter 4d28c5a
Add comment about implications of home discovery failures
allenporter 4b06b87
Fix lint errors in test_number.py
allenporter 652f405
Move get_devices out of the exception catch path
allenporter cb92699
Add exception handling in coordinator setup
allenporter b5a2181
Add comment describing update behavior
allenporter 6b289a3
Update coordinator based on feedback
allenporter 4c3ab76
Update style improvements
allenporter c42ebc6
Update exception handling
allenporter 54a9775
Rename washing machine and wet dry vac coordinators
allenporter ddf9acf
Remove unnecessary patches
allenporter 2707ed0
Add tests that entity staes from the traits are refreshed
allenporter File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -9,16 +9,14 @@ | |
| from typing import Any | ||
|
|
||
| from roborock import ( | ||
| HomeDataRoom, | ||
| RoborockException, | ||
| RoborockInvalidCredentials, | ||
| RoborockInvalidUserAgreement, | ||
| RoborockNoUserAgreement, | ||
| ) | ||
| from roborock.data import DeviceData, HomeDataDevice, HomeDataProduct, UserData | ||
| from roborock.version_1_apis.roborock_mqtt_client_v1 import RoborockMqttClientV1 | ||
| from roborock.version_a01_apis import RoborockMqttClientA01 | ||
| from roborock.web_api import RoborockApiClient | ||
| from roborock.data import UserData | ||
| from roborock.devices.device import RoborockDevice | ||
| from roborock.devices.device_manager import UserParams, create_device_manager | ||
|
|
||
| from homeassistant.const import CONF_USERNAME, EVENT_HOMEASSISTANT_STOP | ||
| from homeassistant.core import HomeAssistant | ||
|
|
@@ -32,8 +30,10 @@ | |
| RoborockCoordinators, | ||
| RoborockDataUpdateCoordinator, | ||
| RoborockDataUpdateCoordinatorA01, | ||
| RoborockWashingMachineUpdateCoordinator, | ||
| RoborockWetDryVacUpdateCoordinator, | ||
| ) | ||
| from .roborock_storage import async_remove_map_storage | ||
| from .roborock_storage import CacheStore, async_remove_map_storage | ||
|
|
||
| SCAN_INTERVAL = timedelta(seconds=30) | ||
|
|
||
|
|
@@ -44,14 +44,18 @@ async def async_setup_entry(hass: HomeAssistant, entry: RoborockConfigEntry) -> | |
| """Set up roborock from a config entry.""" | ||
|
|
||
| user_data = UserData.from_dict(entry.data[CONF_USER_DATA]) | ||
| api_client = RoborockApiClient( | ||
| entry.data[CONF_USERNAME], | ||
| entry.data[CONF_BASE_URL], | ||
| session=async_get_clientsession(hass), | ||
| user_params = UserParams( | ||
| username=entry.data[CONF_USERNAME], | ||
| user_data=user_data, | ||
| base_url=entry.data[CONF_BASE_URL], | ||
| ) | ||
| _LOGGER.debug("Getting home data") | ||
| cache = CacheStore(hass, entry.entry_id) | ||
| try: | ||
| home_data = await api_client.get_home_data_v3(user_data) | ||
| device_manager = await create_device_manager( | ||
| user_params, | ||
| cache=cache, | ||
| session=async_get_clientsession(hass), | ||
| ) | ||
| except RoborockInvalidCredentials as err: | ||
| raise ConfigEntryAuthFailed( | ||
| "Invalid credentials", | ||
|
|
@@ -75,29 +79,15 @@ async def async_setup_entry(hass: HomeAssistant, entry: RoborockConfigEntry) -> | |
| translation_domain=DOMAIN, | ||
| translation_key="home_data_fail", | ||
| ) from err | ||
| devices = await device_manager.get_devices() | ||
| _LOGGER.debug("Device manager found %d devices", len(devices)) | ||
| for device in devices: | ||
| entry.async_on_unload(device.close) | ||
|
|
||
| _LOGGER.debug("Got home data %s", home_data) | ||
| all_devices: list[HomeDataDevice] = home_data.devices + home_data.received_devices | ||
| device_map: dict[str, HomeDataDevice] = { | ||
| device.duid: device for device in all_devices | ||
| } | ||
| product_info: dict[str, HomeDataProduct] = { | ||
| product.id: product for product in home_data.products | ||
| } | ||
| # Get a Coordinator if the device is available or if we have connected to the device before | ||
| coordinators = await asyncio.gather( | ||
| *build_setup_functions( | ||
| hass, | ||
| entry, | ||
| device_map, | ||
| user_data, | ||
| product_info, | ||
| home_data.rooms, | ||
| api_client, | ||
| ), | ||
| *build_setup_functions(hass, entry, devices, user_data), | ||
| return_exceptions=True, | ||
| ) | ||
| # Valid coordinators are those where we had networking cached or we could get networking | ||
| v1_coords = [ | ||
| coord | ||
| for coord in coordinators | ||
|
|
@@ -115,17 +105,15 @@ async def async_setup_entry(hass: HomeAssistant, entry: RoborockConfigEntry) -> | |
| translation_key="no_coordinators", | ||
| ) | ||
| valid_coordinators = RoborockCoordinators(v1_coords, a01_coords) | ||
| await asyncio.gather( | ||
| *(coord.refresh_coordinator_map() for coord in valid_coordinators.v1) | ||
| ) | ||
|
|
||
| async def on_stop(_: Any) -> None: | ||
| _LOGGER.debug("Shutting down roborock") | ||
| await asyncio.gather( | ||
| *( | ||
| coordinator.async_shutdown() | ||
| for coordinator in valid_coordinators.values() | ||
| ) | ||
| ), | ||
| cache.flush(), | ||
| ) | ||
|
|
||
| entry.async_on_unload( | ||
|
|
@@ -138,6 +126,17 @@ async def on_stop(_: Any) -> None: | |
|
|
||
| await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) | ||
|
|
||
| _remove_stale_devices(hass, entry, devices) | ||
|
|
||
| return True | ||
|
|
||
|
|
||
| def _remove_stale_devices( | ||
| hass: HomeAssistant, | ||
| entry: RoborockConfigEntry, | ||
| devices: list[RoborockDevice], | ||
| ) -> None: | ||
| device_map: dict[str, RoborockDevice] = {device.duid: device for device in devices} | ||
| device_registry = dr.async_get(hass) | ||
| device_entries = dr.async_entries_for_config_entry( | ||
| device_registry, config_entry_id=entry.entry_id | ||
|
|
@@ -159,8 +158,6 @@ async def on_stop(_: Any) -> None: | |
| remove_config_entry_id=entry.entry_id, | ||
| ) | ||
|
|
||
| return True | ||
|
|
||
|
|
||
| async def async_migrate_entry(hass: HomeAssistant, entry: RoborockConfigEntry) -> bool: | ||
| """Migrate old configuration entries to the new format.""" | ||
|
|
@@ -190,11 +187,8 @@ async def async_migrate_entry(hass: HomeAssistant, entry: RoborockConfigEntry) - | |
| def build_setup_functions( | ||
| hass: HomeAssistant, | ||
| entry: RoborockConfigEntry, | ||
| device_map: dict[str, HomeDataDevice], | ||
| devices: list[RoborockDevice], | ||
| user_data: UserData, | ||
| product_info: dict[str, HomeDataProduct], | ||
| home_data_rooms: list[HomeDataRoom], | ||
| api_client: RoborockApiClient, | ||
| ) -> list[ | ||
| Coroutine[ | ||
| Any, | ||
|
|
@@ -203,134 +197,45 @@ def build_setup_functions( | |
| ] | ||
| ]: | ||
| """Create a list of setup functions that can later be called asynchronously.""" | ||
| return [ | ||
| setup_device( | ||
| hass, | ||
| entry, | ||
| user_data, | ||
| device, | ||
| product_info[device.product_id], | ||
| home_data_rooms, | ||
| api_client, | ||
| ) | ||
| for device in device_map.values() | ||
| ] | ||
|
|
||
| coordinators: list[ | ||
| RoborockDataUpdateCoordinator | RoborockDataUpdateCoordinatorA01 | ||
| ] = [] | ||
| for device in devices: | ||
| _LOGGER.debug("Creating device %s: %s", device.name, device) | ||
| if device.v1_properties is not None: | ||
| coordinators.append( | ||
| RoborockDataUpdateCoordinator(hass, entry, device, device.v1_properties) | ||
| ) | ||
| elif device.dyad is not None: | ||
| coordinators.append( | ||
| RoborockWetDryVacUpdateCoordinator(hass, entry, device, device.dyad) | ||
| ) | ||
| elif device.zeo is not None: | ||
| coordinators.append( | ||
| RoborockWashingMachineUpdateCoordinator(hass, entry, device, device.zeo) | ||
| ) | ||
| else: | ||
| _LOGGER.warning( | ||
| "Not adding device %s because its protocol version %s or category %s is not supported", | ||
| device.duid, | ||
| device.device_info.pv, | ||
| device.product.category.name, | ||
| ) | ||
|
|
||
| async def setup_device( | ||
| hass: HomeAssistant, | ||
| entry: RoborockConfigEntry, | ||
| user_data: UserData, | ||
| device: HomeDataDevice, | ||
| product_info: HomeDataProduct, | ||
| home_data_rooms: list[HomeDataRoom], | ||
| api_client: RoborockApiClient, | ||
| ) -> RoborockDataUpdateCoordinator | RoborockDataUpdateCoordinatorA01 | None: | ||
| """Set up a coordinator for a given device.""" | ||
| if device.pv == "1.0": | ||
| return await setup_device_v1( | ||
| hass, entry, user_data, device, product_info, home_data_rooms, api_client | ||
| ) | ||
| if device.pv == "A01": | ||
| return await setup_device_a01(hass, entry, user_data, device, product_info) | ||
| _LOGGER.warning( | ||
| "Not adding device %s because its protocol version %s or category %s is not supported", | ||
| device.duid, | ||
| device.pv, | ||
| product_info.category.name, | ||
| ) | ||
| return None | ||
| return [setup_coordinator(coordinator) for coordinator in coordinators] | ||
|
|
||
|
|
||
| async def setup_device_v1( | ||
| hass: HomeAssistant, | ||
| entry: RoborockConfigEntry, | ||
| user_data: UserData, | ||
| device: HomeDataDevice, | ||
| product_info: HomeDataProduct, | ||
| home_data_rooms: list[HomeDataRoom], | ||
| api_client: RoborockApiClient, | ||
| ) -> RoborockDataUpdateCoordinator | None: | ||
| """Set up a device Coordinator.""" | ||
| mqtt_client = await hass.async_add_executor_job( | ||
| RoborockMqttClientV1, user_data, DeviceData(device, product_info.model) | ||
| ) | ||
| try: | ||
| await mqtt_client.async_connect() | ||
| networking = await mqtt_client.get_networking() | ||
| if networking is None: | ||
| # If the api does not return an error but does return None for | ||
| # get_networking - then we need to go through cache checking. | ||
| raise RoborockException("Networking request returned None.") # noqa: TRY301 | ||
| except RoborockException as err: | ||
| _LOGGER.warning( | ||
| "Not setting up %s because we could not get the network information of the device. " | ||
| "Please confirm it is online and the Roborock servers can communicate with it", | ||
| device.name, | ||
| ) | ||
| _LOGGER.debug(err) | ||
| await mqtt_client.async_release() | ||
| raise | ||
| coordinator = RoborockDataUpdateCoordinator( | ||
| hass, | ||
| entry, | ||
| device, | ||
| networking, | ||
| product_info, | ||
| mqtt_client, | ||
| home_data_rooms, | ||
| api_client, | ||
| user_data, | ||
| ) | ||
| async def setup_coordinator( | ||
| coordinator: RoborockDataUpdateCoordinator | RoborockDataUpdateCoordinatorA01, | ||
| ) -> RoborockDataUpdateCoordinator | RoborockDataUpdateCoordinatorA01 | None: | ||
| """Set up a single coordinator.""" | ||
| try: | ||
| await coordinator.async_config_entry_first_refresh() | ||
| except ConfigEntryNotReady as ex: | ||
| except ConfigEntryNotReady: | ||
| await coordinator.async_shutdown() | ||
| if isinstance(coordinator.api, RoborockMqttClientV1): | ||
| _LOGGER.warning( | ||
| "Not setting up %s because the we failed to get data for the first time using the online client. " | ||
| "Please ensure your Home Assistant instance can communicate with this device. " | ||
| "You may need to open firewall instances on your Home Assistant network and on your Vacuum's network", | ||
| device.name, | ||
| ) | ||
| # Most of the time if we fail to connect using the mqtt client, the problem is due to firewall, | ||
| # but in case if it isn't, the error can be included in debug logs for the user to grab. | ||
| if coordinator.last_exception: | ||
| _LOGGER.debug(coordinator.last_exception) | ||
| raise coordinator.last_exception from ex | ||
| elif coordinator.last_exception: | ||
| # If this is reached, we have verified that we can communicate with the Vacuum locally, | ||
| # so if there is an error here - it is not a communication issue but some other problem | ||
| extra_error = f"Please create an issue with the following error included: {coordinator.last_exception}" | ||
| _LOGGER.warning( | ||
| "Not setting up %s because the coordinator failed to get data for the first time using the " | ||
| "offline client %s", | ||
| device.name, | ||
| extra_error, | ||
| ) | ||
| raise coordinator.last_exception from ex | ||
| return coordinator | ||
|
|
||
|
|
||
| async def setup_device_a01( | ||
| hass: HomeAssistant, | ||
| entry: RoborockConfigEntry, | ||
| user_data: UserData, | ||
| device: HomeDataDevice, | ||
| product_info: HomeDataProduct, | ||
| ) -> RoborockDataUpdateCoordinatorA01 | None: | ||
| """Set up a A01 protocol device.""" | ||
| mqtt_client = await hass.async_add_executor_job( | ||
| RoborockMqttClientA01, | ||
| user_data, | ||
| DeviceData(device, product_info.model), | ||
| product_info.category, | ||
| ) | ||
| coord = RoborockDataUpdateCoordinatorA01( | ||
| hass, entry, device, product_info, mqtt_client | ||
| ) | ||
| await coord.async_config_entry_first_refresh() | ||
| return coord | ||
| raise | ||
| else: | ||
| return coordinator | ||
|
|
||
|
|
||
| async def async_unload_entry(hass: HomeAssistant, entry: RoborockConfigEntry) -> bool: | ||
|
|
@@ -341,3 +246,5 @@ async def async_unload_entry(hass: HomeAssistant, entry: RoborockConfigEntry) -> | |
| async def async_remove_entry(hass: HomeAssistant, entry: RoborockConfigEntry) -> None: | ||
| """Handle removal of an entry.""" | ||
| await async_remove_map_storage(hass, entry.entry_id) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As we don't use the old format anymore, should we remove it in a config entry migration instead? |
||
| store = CacheStore(hass, entry.entry_id) | ||
| await store.async_remove() | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.