Skip to content

Commit

Permalink
Bump Roborock to 17.0 adding device specific support and bugfixes (ho…
Browse files Browse the repository at this point in the history
…me-assistant#92547)

* init commit

* use official version release

* remove options

* moved first refresh to gather

* add extra tests

* remove model_sepcification

* remove old mqtt test

* bump to 13.4

* fix dndtimer

* bump to 14.1

* add status back

* bump to 17.0

* remove error as it is not used

* addressing mr comments

* making enum access use get()

* add check for empty hass data
  • Loading branch information
Lash-L committed May 19, 2023
1 parent aebded0 commit 0ce1117
Show file tree
Hide file tree
Showing 13 changed files with 161 additions and 204 deletions.
69 changes: 46 additions & 23 deletions homeassistant/components/roborock/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,7 @@

from roborock.api import RoborockApiClient
from roborock.cloud_api import RoborockMqttClient
from roborock.containers import HomeDataDevice, RoborockDeviceInfo, UserData
from roborock.exceptions import RoborockException
from roborock.containers import DeviceData, HomeDataDevice, UserData

from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_USERNAME
Expand All @@ -32,39 +31,58 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
_LOGGER.debug("Getting home data")
home_data = await api_client.get_home_data(user_data)
_LOGGER.debug("Got home data %s", home_data)
devices: list[HomeDataDevice] = home_data.devices + home_data.received_devices
device_map: dict[str, HomeDataDevice] = {
device.duid: device for device in home_data.devices + home_data.received_devices
}
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_client = RoborockMqttClient(
user_data, {device.duid: RoborockDeviceInfo(device) for device in devices}
)
mqtt_clients = [
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(device.duid) for device in devices)
*(mqtt_client.get_networking() for mqtt_client in mqtt_clients)
)
network_info = {
device.duid: result
for device, result in zip(devices, network_results)
for device, result in zip(device_map.values(), network_results)
if result is not None
}
try:
await mqtt_client.async_disconnect()
except RoborockException as err:
_LOGGER.warning("Failed disconnecting from the mqtt server %s", err)
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"
)

product_info = {product.id: product for product in home_data.products}
coordinator = RoborockDataUpdateCoordinator(
hass,
devices,
network_info,
product_info,
coordinator_map: dict[str, RoborockDataUpdateCoordinator] = {}
for device_id, device in device_map.items():
coordinator_map[device_id] = RoborockDataUpdateCoordinator(
hass,
device,
network_info[device_id],
product_info[device.product_id],
)
# If one device update fails - we still want to set up other devices
await asyncio.gather(
*(
coordinator.async_config_entry_first_refresh()
for coordinator in coordinator_map.values()
),
return_exceptions=True,
)
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = {
device_id: coordinator
for device_id, coordinator in coordinator_map.items()
if coordinator.last_update_success
} # Only add coordinators that succeeded

await coordinator.async_config_entry_first_refresh()

hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator
if not hass.data[DOMAIN][entry.entry_id]:
# Don't start if no coordinators succeeded.
raise ConfigEntryNotReady("There are no devices that can currently be reached.")

await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)

Expand All @@ -75,7 +93,12 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Handle removal of an entry."""
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
if unload_ok:
await hass.data[DOMAIN][entry.entry_id].release()
await asyncio.gather(
*(
coordinator.release()
for coordinator in hass.data[DOMAIN][entry.entry_id].values()
)
)
hass.data[DOMAIN].pop(entry.entry_id)

return unload_ok
67 changes: 22 additions & 45 deletions homeassistant/components/roborock/coordinator.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,13 @@
"""Roborock Coordinator."""
from __future__ import annotations

import asyncio
from datetime import timedelta
import logging

from roborock.containers import (
HomeDataDevice,
HomeDataProduct,
NetworkInfo,
RoborockLocalDeviceInfo,
)
from roborock.containers import DeviceData, HomeDataDevice, HomeDataProduct, NetworkInfo
from roborock.exceptions import RoborockException
from roborock.local_api import RoborockLocalClient
from roborock.typing import DeviceProp
from roborock.roborock_typing import DeviceProp

from homeassistant.core import HomeAssistant
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
Expand All @@ -26,61 +20,44 @@
_LOGGER = logging.getLogger(__name__)


class RoborockDataUpdateCoordinator(DataUpdateCoordinator[dict[str, DeviceProp]]):
class RoborockDataUpdateCoordinator(DataUpdateCoordinator[DeviceProp]):
"""Class to manage fetching data from the API."""

def __init__(
self,
hass: HomeAssistant,
devices: list[HomeDataDevice],
devices_networking: dict[str, NetworkInfo],
product_info: dict[str, HomeDataProduct],
device: HomeDataDevice,
device_networking: NetworkInfo,
product_info: HomeDataProduct,
) -> None:
"""Initialize."""
super().__init__(hass, _LOGGER, name=DOMAIN, update_interval=SCAN_INTERVAL)
local_devices_info: dict[str, RoborockLocalDeviceInfo] = {}
hass_devices_info: dict[str, RoborockHassDeviceInfo] = {}
for device in devices:
if not (networking := devices_networking.get(device.duid)):
_LOGGER.warning("Device %s is offline and cannot be setup", device.duid)
continue
hass_devices_info[device.duid] = RoborockHassDeviceInfo(
device,
networking,
product_info[device.product_id],
DeviceProp(),
)
local_devices_info[device.duid] = RoborockLocalDeviceInfo(
device, networking
)
self.api = RoborockLocalClient(local_devices_info)
self.devices_info = hass_devices_info
self.device_info = RoborockHassDeviceInfo(
device,
device_networking,
product_info,
DeviceProp(),
)
device_info = DeviceData(device, product_info.model, device_networking.ip)
self.api = RoborockLocalClient(device_info)

async def release(self) -> None:
"""Disconnect from API."""
await self.api.async_disconnect()

async def _update_device_prop(self, device_info: RoborockHassDeviceInfo) -> None:
async def _update_device_prop(self) -> None:
"""Update device properties."""
device_prop = await self.api.get_prop(device_info.device.duid)
device_prop = await self.api.get_prop()
if device_prop:
if device_info.props:
device_info.props.update(device_prop)
if self.device_info.props:
self.device_info.props.update(device_prop)
else:
device_info.props = device_prop
self.device_info.props = device_prop

async def _async_update_data(self) -> dict[str, DeviceProp]:
async def _async_update_data(self) -> DeviceProp:
"""Update data via library."""
try:
await asyncio.gather(
*(
self._update_device_prop(device_info)
for device_info in self.devices_info.values()
)
)
await self._update_device_prop()
except RoborockException as ex:
raise UpdateFailed(ex) from ex
return {
device_id: device_info.props
for device_id, device_info in self.devices_info.items()
}
return self.device_info.props
36 changes: 17 additions & 19 deletions homeassistant/components/roborock/device.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,15 @@
from typing import Any

from roborock.containers import Status
from roborock.typing import RoborockCommand
from roborock.exceptions import RoborockException
from roborock.roborock_typing import RoborockCommand

from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers.update_coordinator import CoordinatorEntity

from . import RoborockDataUpdateCoordinator
from .const import DOMAIN
from .models import RoborockHassDeviceInfo


class RoborockCoordinatedEntity(CoordinatorEntity[RoborockDataUpdateCoordinator]):
Expand All @@ -21,46 +22,43 @@ class RoborockCoordinatedEntity(CoordinatorEntity[RoborockDataUpdateCoordinator]
def __init__(
self,
unique_id: str,
device_info: RoborockHassDeviceInfo,
coordinator: RoborockDataUpdateCoordinator,
) -> None:
"""Initialize the coordinated Roborock Device."""
super().__init__(coordinator)
self._attr_unique_id = unique_id
self._device_name = device_info.device.name
self._device_id = device_info.device.duid
self._device_model = device_info.product.model
self._fw_version = device_info.device.fv

@property
def _device_status(self) -> Status:
"""Return the status of the device."""
data = self.coordinator.data
if data:
device_data = data.get(self._device_id)
if device_data:
status = device_data.status
if status:
return status
status = data.status
if status:
return status
return Status({})

@property
def device_info(self) -> DeviceInfo:
"""Return the device info."""
return DeviceInfo(
name=self._device_name,
identifiers={(DOMAIN, self._device_id)},
name=self.coordinator.device_info.device.name,
identifiers={(DOMAIN, self.coordinator.device_info.device.duid)},
manufacturer="Roborock",
model=self._device_model,
sw_version=self._fw_version,
model=self.coordinator.device_info.product.model,
sw_version=self.coordinator.device_info.device.fv,
)

async def send(
self, command: RoborockCommand, params: dict[str, Any] | list[Any] | None = None
) -> dict:
"""Send a command to a vacuum cleaner."""
response = await self.coordinator.api.send_command(
self._device_id, command, params
)
try:
response = await self.coordinator.api.send_command(command, params)
except RoborockException as err:
raise HomeAssistantError(
f"Error while calling {command.name} with {params}"
) from err

await self.coordinator.async_request_refresh()
return response
2 changes: 1 addition & 1 deletion homeassistant/components/roborock/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/roborock",
"iot_class": "local_polling",
"loggers": ["roborock"],
"requirements": ["python-roborock==0.8.3"]
"requirements": ["python-roborock==0.17.0"]
}
2 changes: 1 addition & 1 deletion homeassistant/components/roborock/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
from dataclasses import dataclass

from roborock.containers import HomeDataDevice, HomeDataProduct, NetworkInfo
from roborock.typing import DeviceProp
from roborock.roborock_typing import DeviceProp


@dataclass
Expand Down
Loading

0 comments on commit 0ce1117

Please sign in to comment.