Bump Roborock to 17.0 adding device specific support and bugfixes (#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
This commit is contained in:
Luke 2023-05-18 23:55:39 -04:00 committed by GitHub
parent aebded049b
commit 0ce1117287
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 161 additions and 204 deletions

View File

@ -7,8 +7,7 @@ import logging
from roborock.api import RoborockApiClient from roborock.api import RoborockApiClient
from roborock.cloud_api import RoborockMqttClient from roborock.cloud_api import RoborockMqttClient
from roborock.containers import HomeDataDevice, RoborockDeviceInfo, UserData from roborock.containers import DeviceData, HomeDataDevice, UserData
from roborock.exceptions import RoborockException
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_USERNAME from homeassistant.const import CONF_USERNAME
@ -32,39 +31,58 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
_LOGGER.debug("Getting home data") _LOGGER.debug("Getting home data")
home_data = await api_client.get_home_data(user_data) home_data = await api_client.get_home_data(user_data)
_LOGGER.debug("Got home data %s", home_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. # 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( mqtt_clients = [
user_data, {device.duid: RoborockDeviceInfo(device) for device in devices} RoborockMqttClient(
user_data, DeviceData(device, product_info[device.product_id].model)
) )
for device in device_map.values()
]
network_results = await asyncio.gather( 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 = { network_info = {
device.duid: result 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 if result is not None
} }
try: await asyncio.gather(
await mqtt_client.async_disconnect() *(mqtt_client.async_disconnect() for mqtt_client in mqtt_clients),
except RoborockException as err: return_exceptions=True,
_LOGGER.warning("Failed disconnecting from the mqtt server %s", err) )
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"
) )
coordinator_map: dict[str, RoborockDataUpdateCoordinator] = {}
product_info = {product.id: product for product in home_data.products} for device_id, device in device_map.items():
coordinator = RoborockDataUpdateCoordinator( coordinator_map[device_id] = RoborockDataUpdateCoordinator(
hass, hass,
devices, device,
network_info, network_info[device_id],
product_info, 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() if not hass.data[DOMAIN][entry.entry_id]:
# Don't start if no coordinators succeeded.
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator raise ConfigEntryNotReady("There are no devices that can currently be reached.")
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
@ -75,7 +93,12 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Handle removal of an entry.""" """Handle removal of an entry."""
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
if unload_ok: 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) hass.data[DOMAIN].pop(entry.entry_id)
return unload_ok return unload_ok

View File

@ -1,19 +1,13 @@
"""Roborock Coordinator.""" """Roborock Coordinator."""
from __future__ import annotations from __future__ import annotations
import asyncio
from datetime import timedelta from datetime import timedelta
import logging import logging
from roborock.containers import ( from roborock.containers import DeviceData, HomeDataDevice, HomeDataProduct, NetworkInfo
HomeDataDevice,
HomeDataProduct,
NetworkInfo,
RoborockLocalDeviceInfo,
)
from roborock.exceptions import RoborockException from roborock.exceptions import RoborockException
from roborock.local_api import RoborockLocalClient from roborock.local_api import RoborockLocalClient
from roborock.typing import DeviceProp from roborock.roborock_typing import DeviceProp
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
@ -26,61 +20,44 @@ SCAN_INTERVAL = timedelta(seconds=30)
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
class RoborockDataUpdateCoordinator(DataUpdateCoordinator[dict[str, DeviceProp]]): class RoborockDataUpdateCoordinator(DataUpdateCoordinator[DeviceProp]):
"""Class to manage fetching data from the API.""" """Class to manage fetching data from the API."""
def __init__( def __init__(
self, self,
hass: HomeAssistant, hass: HomeAssistant,
devices: list[HomeDataDevice], device: HomeDataDevice,
devices_networking: dict[str, NetworkInfo], device_networking: NetworkInfo,
product_info: dict[str, HomeDataProduct], product_info: HomeDataProduct,
) -> None: ) -> None:
"""Initialize.""" """Initialize."""
super().__init__(hass, _LOGGER, name=DOMAIN, update_interval=SCAN_INTERVAL) super().__init__(hass, _LOGGER, name=DOMAIN, update_interval=SCAN_INTERVAL)
local_devices_info: dict[str, RoborockLocalDeviceInfo] = {} self.device_info = RoborockHassDeviceInfo(
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, device,
networking, device_networking,
product_info[device.product_id], product_info,
DeviceProp(), DeviceProp(),
) )
local_devices_info[device.duid] = RoborockLocalDeviceInfo( device_info = DeviceData(device, product_info.model, device_networking.ip)
device, networking self.api = RoborockLocalClient(device_info)
)
self.api = RoborockLocalClient(local_devices_info)
self.devices_info = hass_devices_info
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()
async def _update_device_prop(self, device_info: RoborockHassDeviceInfo) -> None: async def _update_device_prop(self) -> None:
"""Update device properties.""" """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_prop:
if device_info.props: if self.device_info.props:
device_info.props.update(device_prop) self.device_info.props.update(device_prop)
else: 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.""" """Update data via library."""
try: try:
await asyncio.gather( await self._update_device_prop()
*(
self._update_device_prop(device_info)
for device_info in self.devices_info.values()
)
)
except RoborockException as ex: except RoborockException as ex:
raise UpdateFailed(ex) from ex raise UpdateFailed(ex) from ex
return { return self.device_info.props
device_id: device_info.props
for device_id, device_info in self.devices_info.items()
}

View File

@ -3,14 +3,15 @@
from typing import Any from typing import Any
from roborock.containers import Status 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.entity import DeviceInfo
from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.helpers.update_coordinator import CoordinatorEntity
from . import RoborockDataUpdateCoordinator from . import RoborockDataUpdateCoordinator
from .const import DOMAIN from .const import DOMAIN
from .models import RoborockHassDeviceInfo
class RoborockCoordinatedEntity(CoordinatorEntity[RoborockDataUpdateCoordinator]): class RoborockCoordinatedEntity(CoordinatorEntity[RoborockDataUpdateCoordinator]):
@ -21,25 +22,18 @@ class RoborockCoordinatedEntity(CoordinatorEntity[RoborockDataUpdateCoordinator]
def __init__( def __init__(
self, self,
unique_id: str, unique_id: str,
device_info: RoborockHassDeviceInfo,
coordinator: RoborockDataUpdateCoordinator, coordinator: RoborockDataUpdateCoordinator,
) -> None: ) -> None:
"""Initialize the coordinated Roborock Device.""" """Initialize the coordinated Roborock Device."""
super().__init__(coordinator) super().__init__(coordinator)
self._attr_unique_id = unique_id 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 @property
def _device_status(self) -> Status: def _device_status(self) -> Status:
"""Return the status of the device.""" """Return the status of the device."""
data = self.coordinator.data data = self.coordinator.data
if data: if data:
device_data = data.get(self._device_id) status = data.status
if device_data:
status = device_data.status
if status: if status:
return status return status
return Status({}) return Status({})
@ -48,19 +42,23 @@ class RoborockCoordinatedEntity(CoordinatorEntity[RoborockDataUpdateCoordinator]
def device_info(self) -> DeviceInfo: def device_info(self) -> DeviceInfo:
"""Return the device info.""" """Return the device info."""
return DeviceInfo( return DeviceInfo(
name=self._device_name, name=self.coordinator.device_info.device.name,
identifiers={(DOMAIN, self._device_id)}, identifiers={(DOMAIN, self.coordinator.device_info.device.duid)},
manufacturer="Roborock", manufacturer="Roborock",
model=self._device_model, model=self.coordinator.device_info.product.model,
sw_version=self._fw_version, sw_version=self.coordinator.device_info.device.fv,
) )
async def send( async def send(
self, command: RoborockCommand, params: dict[str, Any] | list[Any] | None = None self, command: RoborockCommand, params: dict[str, Any] | list[Any] | None = None
) -> dict: ) -> dict:
"""Send a command to a vacuum cleaner.""" """Send a command to a vacuum cleaner."""
response = await self.coordinator.api.send_command( try:
self._device_id, command, params 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() await self.coordinator.async_request_refresh()
return response return response

View File

@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/roborock", "documentation": "https://www.home-assistant.io/integrations/roborock",
"iot_class": "local_polling", "iot_class": "local_polling",
"loggers": ["roborock"], "loggers": ["roborock"],
"requirements": ["python-roborock==0.8.3"] "requirements": ["python-roborock==0.17.0"]
} }

View File

@ -2,7 +2,7 @@
from dataclasses import dataclass from dataclasses import dataclass
from roborock.containers import HomeDataDevice, HomeDataProduct, NetworkInfo from roborock.containers import HomeDataDevice, HomeDataProduct, NetworkInfo
from roborock.typing import DeviceProp from roborock.roborock_typing import DeviceProp
@dataclass @dataclass

View File

@ -2,31 +2,32 @@
from collections.abc import Callable from collections.abc import Callable
from dataclasses import dataclass from dataclasses import dataclass
from roborock.code_mappings import RoborockMopIntensityCode, RoborockMopModeCode
from roborock.containers import Status from roborock.containers import Status
from roborock.exceptions import RoborockException from roborock.roborock_typing import RoborockCommand
from roborock.typing import RoborockCommand
from homeassistant.components.select import SelectEntity, SelectEntityDescription from homeassistant.components.select import SelectEntity, SelectEntityDescription
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.util import slugify from homeassistant.util import slugify
from .const import DOMAIN from .const import DOMAIN
from .coordinator import RoborockDataUpdateCoordinator from .coordinator import RoborockDataUpdateCoordinator
from .device import RoborockCoordinatedEntity from .device import RoborockCoordinatedEntity
from .models import RoborockHassDeviceInfo
@dataclass @dataclass
class RoborockSelectDescriptionMixin: class RoborockSelectDescriptionMixin:
"""Define an entity description mixin for select entities.""" """Define an entity description mixin for select entities."""
# The command that the select entity will send to the api.
api_command: RoborockCommand api_command: RoborockCommand
# Gets the current value of the select entity.
value_fn: Callable[[Status], str] value_fn: Callable[[Status], str]
options_lambda: Callable[[str], list[int]] # Gets all options of the select entity.
options_lambda: Callable[[Status], list[str]]
# Takes the value from the select entiy and converts it for the api.
parameter_lambda: Callable[[str, Status], list[int]]
@dataclass @dataclass
@ -40,22 +41,20 @@ SELECT_DESCRIPTIONS: list[RoborockSelectDescription] = [
RoborockSelectDescription( RoborockSelectDescription(
key="water_box_mode", key="water_box_mode",
translation_key="mop_intensity", translation_key="mop_intensity",
options=RoborockMopIntensityCode.values(),
api_command=RoborockCommand.SET_WATER_BOX_CUSTOM_MODE, api_command=RoborockCommand.SET_WATER_BOX_CUSTOM_MODE,
value_fn=lambda data: data.water_box_mode, value_fn=lambda data: data.water_box_mode.name,
options_lambda=lambda data: [ options_lambda=lambda data: data.water_box_mode.keys()
k for k, v in RoborockMopIntensityCode.items() if v == data if data.water_box_mode
], else None,
parameter_lambda=lambda key, status: [status.water_box_mode.as_dict().get(key)],
), ),
RoborockSelectDescription( RoborockSelectDescription(
key="mop_mode", key="mop_mode",
translation_key="mop_mode", translation_key="mop_mode",
options=RoborockMopModeCode.values(),
api_command=RoborockCommand.SET_MOP_MODE, api_command=RoborockCommand.SET_MOP_MODE,
value_fn=lambda data: data.mop_mode, value_fn=lambda data: data.mop_mode.name,
options_lambda=lambda data: [ options_lambda=lambda data: data.mop_mode.keys() if data.mop_mode else None,
k for k, v in RoborockMopModeCode.items() if v == data parameter_lambda=lambda key, status: [status.mop_mode.as_dict().get(key)],
],
), ),
] ]
@ -67,18 +66,18 @@ async def async_setup_entry(
) -> None: ) -> None:
"""Set up Roborock select platform.""" """Set up Roborock select platform."""
coordinator: RoborockDataUpdateCoordinator = hass.data[DOMAIN][ coordinators: dict[str, RoborockDataUpdateCoordinator] = hass.data[DOMAIN][
config_entry.entry_id config_entry.entry_id
] ]
async_add_entities( async_add_entities(
RoborockSelectEntity( RoborockSelectEntity(
f"{description.key}_{slugify(device_id)}", f"{description.key}_{slugify(device_id)}",
device_info,
coordinator, coordinator,
description, description,
) )
for device_id, device_info in coordinator.devices_info.items() for device_id, coordinator in coordinators.items()
for description in SELECT_DESCRIPTIONS for description in SELECT_DESCRIPTIONS
if description.options_lambda(coordinator.device_info.props.status) is not None
) )
@ -90,25 +89,20 @@ class RoborockSelectEntity(RoborockCoordinatedEntity, SelectEntity):
def __init__( def __init__(
self, self,
unique_id: str, unique_id: str,
device_info: RoborockHassDeviceInfo,
coordinator: RoborockDataUpdateCoordinator, coordinator: RoborockDataUpdateCoordinator,
entity_description: RoborockSelectDescription, entity_description: RoborockSelectDescription,
) -> None: ) -> None:
"""Create a select entity.""" """Create a select entity."""
self.entity_description = entity_description self.entity_description = entity_description
super().__init__(unique_id, device_info, coordinator) super().__init__(unique_id, coordinator)
self._attr_options = self.entity_description.options_lambda(self._device_status)
async def async_select_option(self, option: str) -> None: async def async_select_option(self, option: str) -> None:
"""Set the mop intensity.""" """Set the option."""
try:
await self.send( await self.send(
self.entity_description.api_command, self.entity_description.api_command,
self.entity_description.options_lambda(option), self.entity_description.parameter_lambda(option, self._device_status),
) )
except RoborockException as err:
raise HomeAssistantError(
f"Error while setting {self.entity_description.key} to {option}"
) from err
@property @property
def current_option(self) -> str | None: def current_option(self) -> str | None:

View File

@ -34,7 +34,8 @@
"standard": "Standard", "standard": "Standard",
"deep": "Deep", "deep": "Deep",
"deep_plus": "Deep+", "deep_plus": "Deep+",
"custom": "Custom" "custom": "Custom",
"fast": "Fast"
} }
}, },
"mop_intensity": { "mop_intensity": {

View File

@ -1,8 +1,8 @@
"""Support for Roborock vacuum class.""" """Support for Roborock vacuum class."""
from typing import Any from typing import Any
from roborock.code_mappings import RoborockFanPowerCode, RoborockStateCode from roborock.code_mappings import RoborockStateCode
from roborock.typing import RoborockCommand from roborock.roborock_typing import RoborockCommand
from homeassistant.components.vacuum import ( from homeassistant.components.vacuum import (
STATE_CLEANING, STATE_CLEANING,
@ -22,51 +22,46 @@ from homeassistant.util import slugify
from .const import DOMAIN from .const import DOMAIN
from .coordinator import RoborockDataUpdateCoordinator from .coordinator import RoborockDataUpdateCoordinator
from .device import RoborockCoordinatedEntity from .device import RoborockCoordinatedEntity
from .models import RoborockHassDeviceInfo
STATE_CODE_TO_STATE = { STATE_CODE_TO_STATE = {
RoborockStateCode["1"]: STATE_IDLE, # "Starting" RoborockStateCode.starting: STATE_IDLE, # "Starting"
RoborockStateCode["2"]: STATE_IDLE, # "Charger disconnected" RoborockStateCode.charger_disconnected: STATE_IDLE, # "Charger disconnected"
RoborockStateCode["3"]: STATE_IDLE, # "Idle" RoborockStateCode.idle: STATE_IDLE, # "Idle"
RoborockStateCode["4"]: STATE_CLEANING, # "Remote control active" RoborockStateCode.remote_control_active: STATE_CLEANING, # "Remote control active"
RoborockStateCode["5"]: STATE_CLEANING, # "Cleaning" RoborockStateCode.cleaning: STATE_CLEANING, # "Cleaning"
RoborockStateCode["6"]: STATE_RETURNING, # "Returning home" RoborockStateCode.returning_home: STATE_RETURNING, # "Returning home"
RoborockStateCode["7"]: STATE_CLEANING, # "Manual mode" RoborockStateCode.manual_mode: STATE_CLEANING, # "Manual mode"
RoborockStateCode["8"]: STATE_DOCKED, # "Charging" RoborockStateCode.charging: STATE_DOCKED, # "Charging"
RoborockStateCode["9"]: STATE_ERROR, # "Charging problem" RoborockStateCode.charging_problem: STATE_ERROR, # "Charging problem"
RoborockStateCode["10"]: STATE_PAUSED, # "Paused" RoborockStateCode.paused: STATE_PAUSED, # "Paused"
RoborockStateCode["11"]: STATE_CLEANING, # "Spot cleaning" RoborockStateCode.spot_cleaning: STATE_CLEANING, # "Spot cleaning"
RoborockStateCode["12"]: STATE_ERROR, # "Error" RoborockStateCode.error: STATE_ERROR, # "Error"
RoborockStateCode["13"]: STATE_IDLE, # "Shutting down" RoborockStateCode.shutting_down: STATE_IDLE, # "Shutting down"
RoborockStateCode["14"]: STATE_DOCKED, # "Updating" RoborockStateCode.updating: STATE_DOCKED, # "Updating"
RoborockStateCode["15"]: STATE_RETURNING, # "Docking" RoborockStateCode.docking: STATE_RETURNING, # "Docking"
RoborockStateCode["16"]: STATE_CLEANING, # "Going to target" RoborockStateCode.going_to_target: STATE_CLEANING, # "Going to target"
RoborockStateCode["17"]: STATE_CLEANING, # "Zoned cleaning" RoborockStateCode.zoned_cleaning: STATE_CLEANING, # "Zoned cleaning"
RoborockStateCode["18"]: STATE_CLEANING, # "Segment cleaning" RoborockStateCode.segment_cleaning: STATE_CLEANING, # "Segment cleaning"
RoborockStateCode["22"]: STATE_DOCKED, # "Emptying the bin" on s7+ RoborockStateCode.emptying_the_bin: STATE_DOCKED, # "Emptying the bin" on s7+
RoborockStateCode["23"]: STATE_DOCKED, # "Washing the mop" on s7maxV RoborockStateCode.washing_the_mop: STATE_DOCKED, # "Washing the mop" on s7maxV
RoborockStateCode["26"]: STATE_RETURNING, # "Going to wash the mop" on s7maxV RoborockStateCode.going_to_wash_the_mop: STATE_RETURNING, # "Going to wash the mop" on s7maxV
RoborockStateCode["100"]: STATE_DOCKED, # "Charging complete" RoborockStateCode.charging_complete: STATE_DOCKED, # "Charging complete"
RoborockStateCode["101"]: STATE_ERROR, # "Device offline" RoborockStateCode.device_offline: STATE_ERROR, # "Device offline"
} }
ATTR_STATUS = "status"
ATTR_ERROR = "error"
async def async_setup_entry( async def async_setup_entry(
hass: HomeAssistant, hass: HomeAssistant,
config_entry: ConfigEntry, config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback, async_add_entities: AddEntitiesCallback,
) -> None: ) -> None:
"""Set up the Roborock sensor.""" """Set up the Roborock sensor."""
coordinator: RoborockDataUpdateCoordinator = hass.data[DOMAIN][ coordinators: dict[str, RoborockDataUpdateCoordinator] = hass.data[DOMAIN][
config_entry.entry_id config_entry.entry_id
] ]
async_add_entities( async_add_entities(
RoborockVacuum(slugify(device_id), device_info, coordinator) RoborockVacuum(slugify(device_id), coordinator)
for device_id, device_info in coordinator.devices_info.items() for device_id, coordinator in coordinators.items()
) )
@ -87,28 +82,22 @@ class RoborockVacuum(RoborockCoordinatedEntity, StateVacuumEntity):
| VacuumEntityFeature.STATE | VacuumEntityFeature.STATE
| VacuumEntityFeature.START | VacuumEntityFeature.START
) )
_attr_fan_speed_list = RoborockFanPowerCode.values()
def __init__( def __init__(
self, self,
unique_id: str, unique_id: str,
device: RoborockHassDeviceInfo,
coordinator: RoborockDataUpdateCoordinator, coordinator: RoborockDataUpdateCoordinator,
) -> None: ) -> None:
"""Initialize a vacuum.""" """Initialize a vacuum."""
StateVacuumEntity.__init__(self) StateVacuumEntity.__init__(self)
RoborockCoordinatedEntity.__init__(self, unique_id, device, coordinator) RoborockCoordinatedEntity.__init__(self, unique_id, coordinator)
self._attr_fan_speed_list = self._device_status.fan_power.keys()
@property @property
def state(self) -> str | None: def state(self) -> str | None:
"""Return the status of the vacuum cleaner.""" """Return the status of the vacuum cleaner."""
return STATE_CODE_TO_STATE.get(self._device_status.state) return STATE_CODE_TO_STATE.get(self._device_status.state)
@property
def status(self) -> str | None:
"""Return the status of the vacuum cleaner."""
return self._device_status.status
@property @property
def battery_level(self) -> int | None: def battery_level(self) -> int | None:
"""Return the battery level of the vacuum cleaner.""" """Return the battery level of the vacuum cleaner."""
@ -117,12 +106,12 @@ class RoborockVacuum(RoborockCoordinatedEntity, StateVacuumEntity):
@property @property
def fan_speed(self) -> str | None: def fan_speed(self) -> str | None:
"""Return the fan speed of the vacuum cleaner.""" """Return the fan speed of the vacuum cleaner."""
return self._device_status.fan_power return self._device_status.fan_power.name
@property @property
def error(self) -> str | None: def status(self) -> str | None:
"""Get the error str if an error code exists.""" """Return the status of the vacuum cleaner."""
return self._device_status.error return self._device_status.state.name
async def async_start(self) -> None: async def async_start(self) -> None:
"""Start the vacuum.""" """Start the vacuum."""
@ -152,11 +141,11 @@ class RoborockVacuum(RoborockCoordinatedEntity, StateVacuumEntity):
"""Set vacuum fan speed.""" """Set vacuum fan speed."""
await self.send( await self.send(
RoborockCommand.SET_CUSTOM_MODE, RoborockCommand.SET_CUSTOM_MODE,
[k for k, v in RoborockFanPowerCode.items() if v == fan_speed], [self._device_status.fan_power.as_dict().get(fan_speed)],
) )
await self.coordinator.async_request_refresh() await self.coordinator.async_request_refresh()
async def async_start_pause(self): async def async_start_pause(self) -> None:
"""Start, pause or resume the cleaning task.""" """Start, pause or resume the cleaning task."""
if self.state == STATE_CLEANING: if self.state == STATE_CLEANING:
await self.async_pause() await self.async_pause()

View File

@ -2111,7 +2111,7 @@ python-qbittorrent==0.4.2
python-ripple-api==0.0.3 python-ripple-api==0.0.3
# homeassistant.components.roborock # homeassistant.components.roborock
python-roborock==0.8.3 python-roborock==0.17.0
# homeassistant.components.smarttub # homeassistant.components.smarttub
python-smarttub==0.0.33 python-smarttub==0.0.33

View File

@ -1531,7 +1531,7 @@ python-picnic-api==1.1.0
python-qbittorrent==0.4.2 python-qbittorrent==0.4.2
# homeassistant.components.roborock # homeassistant.components.roborock
python-roborock==0.8.3 python-roborock==0.17.0
# homeassistant.components.smarttub # homeassistant.components.smarttub
python-smarttub==0.0.33 python-smarttub==0.0.33

View File

@ -5,13 +5,13 @@ from roborock.containers import (
CleanRecord, CleanRecord,
CleanSummary, CleanSummary,
Consumable, Consumable,
DNDTimer, DnDTimer,
HomeData, HomeData,
NetworkInfo, NetworkInfo,
Status, S7Status,
UserData, UserData,
) )
from roborock.typing import DeviceProp from roborock.roborock_typing import DeviceProp
# All data is based on a U.S. customer with a Roborock S7 MaxV Ultra # All data is based on a U.S. customer with a Roborock S7 MaxV Ultra
USER_EMAIL = "user@domain.com" USER_EMAIL = "user@domain.com"
@ -311,7 +311,7 @@ CONSUMABLE = Consumable.from_dict(
} }
) )
DND_TIMER = DNDTimer.from_dict( DND_TIMER = DnDTimer.from_dict(
{ {
"start_hour": 22, "start_hour": 22,
"start_minute": 0, "start_minute": 0,
@ -321,7 +321,7 @@ DND_TIMER = DNDTimer.from_dict(
} }
) )
STATUS = Status.from_dict( STATUS = S7Status.from_dict(
{ {
"msg_ver": 2, "msg_ver": 2,
"msg_seq": 458, "msg_seq": 458,
@ -367,7 +367,6 @@ STATUS = Status.from_dict(
"unsave_map_flag": 0, "unsave_map_flag": 0,
} }
) )
PROP = DeviceProp(STATUS, DND_TIMER, CLEAN_SUMMARY, CONSUMABLE, CLEAN_RECORD) PROP = DeviceProp(STATUS, DND_TIMER, CLEAN_SUMMARY, CONSUMABLE, CLEAN_RECORD)
NETWORK_INFO = NetworkInfo( NETWORK_INFO = NetworkInfo(

View File

@ -1,8 +1,6 @@
"""Test for Roborock init.""" """Test for Roborock init."""
from unittest.mock import patch from unittest.mock import patch
from roborock.exceptions import RoborockTimeout
from homeassistant.components.roborock.const import DOMAIN from homeassistant.components.roborock.const import DOMAIN
from homeassistant.config_entries import ConfigEntryState from homeassistant.config_entries import ConfigEntryState
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
@ -10,7 +8,6 @@ from homeassistant.helpers.update_coordinator import UpdateFailed
from homeassistant.setup import async_setup_component from homeassistant.setup import async_setup_component
from tests.common import MockConfigEntry from tests.common import MockConfigEntry
from tests.components.roborock.mock_data import HOME_DATA, NETWORK_INFO
async def test_unload_entry( async def test_unload_entry(
@ -41,23 +38,3 @@ async def test_config_entry_not_ready(
): ):
await async_setup_component(hass, DOMAIN, {}) await async_setup_component(hass, DOMAIN, {})
assert mock_roborock_entry.state is ConfigEntryState.SETUP_RETRY assert mock_roborock_entry.state is ConfigEntryState.SETUP_RETRY
async def test_continue_setup_mqtt_disconnect_fail(
hass: HomeAssistant, mock_roborock_entry: MockConfigEntry
):
"""Test that if disconnect fails, we still continue setting up."""
with patch(
"homeassistant.components.roborock.RoborockApiClient.get_home_data",
return_value=HOME_DATA,
), patch(
"homeassistant.components.roborock.RoborockMqttClient.get_networking",
return_value=NETWORK_INFO,
), patch(
"homeassistant.components.roborock.RoborockMqttClient.async_disconnect",
side_effect=RoborockTimeout(),
), patch(
"homeassistant.components.roborock.RoborockDataUpdateCoordinator.async_config_entry_first_refresh"
):
await async_setup_component(hass, DOMAIN, {})
assert mock_roborock_entry.state is ConfigEntryState.LOADED

View File

@ -5,7 +5,7 @@ from typing import Any
from unittest.mock import patch from unittest.mock import patch
import pytest import pytest
from roborock.typing import RoborockCommand from roborock.roborock_typing import RoborockCommand
from homeassistant.components.vacuum import ( from homeassistant.components.vacuum import (
SERVICE_CLEAN_SPOT, SERVICE_CLEAN_SPOT,
@ -50,7 +50,7 @@ async def test_registry_entries(
( (
SERVICE_SET_FAN_SPEED, SERVICE_SET_FAN_SPEED,
RoborockCommand.SET_CUSTOM_MODE, RoborockCommand.SET_CUSTOM_MODE,
{"fan_speed": "silent"}, {"fan_speed": "quiet"},
[101], [101],
), ),
( (
@ -86,6 +86,5 @@ async def test_commands(
blocking=True, blocking=True,
) )
assert mock_send_command.call_count == 1 assert mock_send_command.call_count == 1
assert mock_send_command.call_args[0][0] == DEVICE_ID assert mock_send_command.call_args[0][0] == command
assert mock_send_command.call_args[0][1] == command assert mock_send_command.call_args[0][1] == called_params
assert mock_send_command.call_args[0][2] == called_params