diff --git a/homeassistant/components/roborock/binary_sensor.py b/homeassistant/components/roborock/binary_sensor.py index a8f6a6fbb4f..203f981e51d 100644 --- a/homeassistant/components/roborock/binary_sensor.py +++ b/homeassistant/components/roborock/binary_sensor.py @@ -26,7 +26,7 @@ from .device import RoborockCoordinatedEntity class RoborockBinarySensorDescriptionMixin: """A class that describes binary sensor entities.""" - value_fn: Callable[[DeviceProp], bool] + value_fn: Callable[[DeviceProp], bool | int | None] @dataclass diff --git a/homeassistant/components/roborock/coordinator.py b/homeassistant/components/roborock/coordinator.py index dd4ef9e052f..30bfc71ea48 100644 --- a/homeassistant/components/roborock/coordinator.py +++ b/homeassistant/components/roborock/coordinator.py @@ -33,7 +33,7 @@ class RoborockDataUpdateCoordinator(DataUpdateCoordinator[DeviceProp]): device: HomeDataDevice, device_networking: NetworkInfo, product_info: HomeDataProduct, - cloud_api: RoborockMqttClient | None = None, + cloud_api: RoborockMqttClient, ) -> None: """Initialize.""" super().__init__(hass, _LOGGER, name=DOMAIN, update_interval=SCAN_INTERVAL) @@ -44,7 +44,9 @@ class RoborockDataUpdateCoordinator(DataUpdateCoordinator[DeviceProp]): DeviceProp(), ) device_data = DeviceData(device, product_info.model, device_networking.ip) - self.api = RoborockLocalClient(device_data) + self.api: RoborockLocalClient | RoborockMqttClient = RoborockLocalClient( + device_data + ) self.cloud_api = cloud_api self.device_info = DeviceInfo( name=self.roborock_device_info.device.name, @@ -59,18 +61,18 @@ class RoborockDataUpdateCoordinator(DataUpdateCoordinator[DeviceProp]): 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): + if isinstance(self.api, RoborockLocalClient): + try: + await self.api.ping() + except RoborockException: _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. + # 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.""" diff --git a/homeassistant/components/roborock/device.py b/homeassistant/components/roborock/device.py index 2b005ecade6..c8f45b40d82 100644 --- a/homeassistant/components/roborock/device.py +++ b/homeassistant/components/roborock/device.py @@ -36,7 +36,7 @@ class RoborockEntity(Entity): def get_cache(self, attribute: CacheableAttribute) -> AttributeCache: """Get an item from the api cache.""" - return self._api.cache.get(attribute) + return self._api.cache[attribute] async def send( self, @@ -45,7 +45,7 @@ class RoborockEntity(Entity): ) -> dict: """Send a command to a vacuum cleaner.""" try: - response = await self._api.send_command(command, params) + response: dict = await self._api.send_command(command, params) except RoborockException as err: raise HomeAssistantError( f"Error while calling {command.name if isinstance(command, RoborockCommand) else command} with {params}" @@ -80,15 +80,11 @@ class RoborockCoordinatedEntity( def _device_status(self) -> Status: """Return the status of the device.""" data = self.coordinator.data - if data: - status = data.status - if status: - return status - return Status({}) + return data.status async def send( self, - command: RoborockCommand, + command: RoborockCommand | str, params: dict[str, Any] | list[Any] | int | None = None, ) -> dict: """Overloads normal send command but refreshes coordinator.""" diff --git a/homeassistant/components/roborock/manifest.json b/homeassistant/components/roborock/manifest.json index 5be48c1f4bf..93f3f18f5fe 100644 --- a/homeassistant/components/roborock/manifest.json +++ b/homeassistant/components/roborock/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/roborock", "iot_class": "local_polling", "loggers": ["roborock"], - "requirements": ["python-roborock==0.35.0"] + "requirements": ["python-roborock==0.36.0"] } diff --git a/homeassistant/components/roborock/number.py b/homeassistant/components/roborock/number.py index 4eaf1464f89..d91606418d9 100644 --- a/homeassistant/components/roborock/number.py +++ b/homeassistant/components/roborock/number.py @@ -74,7 +74,7 @@ async def async_setup_entry( # We need to check if this function is supported by the device. results = await asyncio.gather( *( - coordinator.api.cache.get(description.cache_key).async_value() + coordinator.api.get_from_cache(description.cache_key) for coordinator, description in possible_entities ), return_exceptions=True, diff --git a/homeassistant/components/roborock/select.py b/homeassistant/components/roborock/select.py index 5cf71bb12f4..f4968bf7db9 100644 --- a/homeassistant/components/roborock/select.py +++ b/homeassistant/components/roborock/select.py @@ -24,9 +24,9 @@ class RoborockSelectDescriptionMixin: # The command that the select entity will send to the api. api_command: RoborockCommand # Gets the current value of the select entity. - value_fn: Callable[[Status], str] + value_fn: Callable[[Status], str | None] # Gets all options of the select entity. - options_lambda: Callable[[Status], list[str]] + options_lambda: Callable[[Status], list[str] | None] # Takes the value from the select entiy and converts it for the api. parameter_lambda: Callable[[str, Status], list[int]] @@ -43,21 +43,23 @@ SELECT_DESCRIPTIONS: list[RoborockSelectDescription] = [ key="water_box_mode", translation_key="mop_intensity", api_command=RoborockCommand.SET_WATER_BOX_CUSTOM_MODE, - value_fn=lambda data: data.water_box_mode.name, + value_fn=lambda data: data.water_box_mode_name, entity_category=EntityCategory.CONFIG, options_lambda=lambda data: data.water_box_mode.keys() - if data.water_box_mode + if data.water_box_mode is not None else None, - parameter_lambda=lambda key, status: [status.water_box_mode.as_dict().get(key)], + parameter_lambda=lambda key, status: [status.get_mop_intensity_code(key)], ), RoborockSelectDescription( key="mop_mode", translation_key="mop_mode", api_command=RoborockCommand.SET_MOP_MODE, - value_fn=lambda data: data.mop_mode.name, + value_fn=lambda data: data.mop_mode_name, entity_category=EntityCategory.CONFIG, - options_lambda=lambda data: data.mop_mode.keys() if data.mop_mode else None, - parameter_lambda=lambda key, status: [status.mop_mode.as_dict().get(key)], + options_lambda=lambda data: data.mop_mode.keys() + if data.mop_mode is not None + else None, + parameter_lambda=lambda key, status: [status.get_mop_mode_code(key)], ), ] @@ -74,13 +76,15 @@ async def async_setup_entry( ] async_add_entities( RoborockSelectEntity( - f"{description.key}_{slugify(device_id)}", - coordinator, - description, + f"{description.key}_{slugify(device_id)}", coordinator, description, options ) for device_id, coordinator in coordinators.items() for description in SELECT_DESCRIPTIONS - if description.options_lambda(coordinator.roborock_device_info.props.status) + if ( + options := description.options_lambda( + coordinator.roborock_device_info.props.status + ) + ) is not None ) @@ -95,11 +99,12 @@ class RoborockSelectEntity(RoborockCoordinatedEntity, SelectEntity): unique_id: str, coordinator: RoborockDataUpdateCoordinator, entity_description: RoborockSelectDescription, + options: list[str], ) -> None: """Create a select entity.""" self.entity_description = entity_description super().__init__(unique_id, coordinator) - self._attr_options = self.entity_description.options_lambda(self._device_status) + self._attr_options = options async def async_select_option(self, option: str) -> None: """Set the option.""" diff --git a/homeassistant/components/roborock/sensor.py b/homeassistant/components/roborock/sensor.py index 113e02e4abe..090ab2f233c 100644 --- a/homeassistant/components/roborock/sensor.py +++ b/homeassistant/components/roborock/sensor.py @@ -117,7 +117,7 @@ SENSOR_DESCRIPTIONS = [ icon="mdi:information-outline", device_class=SensorDeviceClass.ENUM, translation_key="status", - value_fn=lambda data: data.status.state.name, + value_fn=lambda data: data.status.state_name, entity_category=EntityCategory.DIAGNOSTIC, options=RoborockStateCode.keys(), ), @@ -142,7 +142,7 @@ SENSOR_DESCRIPTIONS = [ icon="mdi:alert-circle", translation_key="vacuum_error", device_class=SensorDeviceClass.ENUM, - value_fn=lambda data: data.status.error_code.name, + value_fn=lambda data: data.status.error_code_name, entity_category=EntityCategory.DIAGNOSTIC, options=RoborockErrorCode.keys(), ), @@ -157,7 +157,9 @@ SENSOR_DESCRIPTIONS = [ key="last_clean_start", translation_key="last_clean_start", icon="mdi:clock-time-twelve", - value_fn=lambda data: data.last_clean_record.begin_datetime, + value_fn=lambda data: data.last_clean_record.begin_datetime + if data.last_clean_record is not None + else None, entity_category=EntityCategory.DIAGNOSTIC, device_class=SensorDeviceClass.TIMESTAMP, ), @@ -165,7 +167,9 @@ SENSOR_DESCRIPTIONS = [ key="last_clean_end", translation_key="last_clean_end", icon="mdi:clock-time-twelve", - value_fn=lambda data: data.last_clean_record.end_datetime, + value_fn=lambda data: data.last_clean_record.end_datetime + if data.last_clean_record is not None + else None, entity_category=EntityCategory.DIAGNOSTIC, device_class=SensorDeviceClass.TIMESTAMP, ), diff --git a/homeassistant/components/roborock/switch.py b/homeassistant/components/roborock/switch.py index de820ede8fa..3dd7307da72 100644 --- a/homeassistant/components/roborock/switch.py +++ b/homeassistant/components/roborock/switch.py @@ -125,7 +125,7 @@ async def async_setup_entry( # We need to check if this function is supported by the device. results = await asyncio.gather( *( - coordinator.api.cache.get(description.cache_key).async_value() + coordinator.api.get_from_cache(description.cache_key) for coordinator, description in possible_entities ), return_exceptions=True, diff --git a/homeassistant/components/roborock/time.py b/homeassistant/components/roborock/time.py index 5dc98e09352..d02d63597ac 100644 --- a/homeassistant/components/roborock/time.py +++ b/homeassistant/components/roborock/time.py @@ -139,7 +139,7 @@ async def async_setup_entry( # We need to check if this function is supported by the device. results = await asyncio.gather( *( - coordinator.api.cache.get(description.cache_key).async_value() + coordinator.api.get_from_cache(description.cache_key) for coordinator, description in possible_entities ), return_exceptions=True, diff --git a/homeassistant/components/roborock/vacuum.py b/homeassistant/components/roborock/vacuum.py index 804c0826578..0edd8e3ec5a 100644 --- a/homeassistant/components/roborock/vacuum.py +++ b/homeassistant/components/roborock/vacuum.py @@ -93,11 +93,12 @@ class RoborockVacuum(RoborockCoordinatedEntity, StateVacuumEntity): """Initialize a vacuum.""" StateVacuumEntity.__init__(self) RoborockCoordinatedEntity.__init__(self, unique_id, coordinator) - self._attr_fan_speed_list = self._device_status.fan_power.keys() + self._attr_fan_speed_list = self._device_status.fan_power_options @property def state(self) -> str | None: """Return the status of the vacuum cleaner.""" + assert self._device_status.state is not None return STATE_CODE_TO_STATE.get(self._device_status.state) @property @@ -108,7 +109,7 @@ class RoborockVacuum(RoborockCoordinatedEntity, StateVacuumEntity): @property def fan_speed(self) -> str | None: """Return the fan speed of the vacuum cleaner.""" - return self._device_status.fan_power.name + return self._device_status.fan_power_name async def async_start(self) -> None: """Start the vacuum.""" @@ -138,7 +139,7 @@ class RoborockVacuum(RoborockCoordinatedEntity, StateVacuumEntity): """Set vacuum fan speed.""" await self.send( RoborockCommand.SET_CUSTOM_MODE, - [self._device_status.fan_power.as_dict().get(fan_speed)], + [self._device_status.get_fan_speed_code(fan_speed)], ) async def async_start_pause(self) -> None: diff --git a/requirements_all.txt b/requirements_all.txt index b0da7f8d209..6faa18cea85 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2184,7 +2184,7 @@ python-qbittorrent==0.4.3 python-ripple-api==0.0.3 # homeassistant.components.roborock -python-roborock==0.35.0 +python-roborock==0.36.0 # homeassistant.components.smarttub python-smarttub==0.0.35 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 906919a3129..d341c4fca42 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1628,7 +1628,7 @@ python-picnic-api==1.1.0 python-qbittorrent==0.4.3 # homeassistant.components.roborock -python-roborock==0.35.0 +python-roborock==0.36.0 # homeassistant.components.smarttub python-smarttub==0.0.35 diff --git a/tests/components/roborock/snapshots/test_diagnostics.ambr b/tests/components/roborock/snapshots/test_diagnostics.ambr index d8e5f7d4cb2..6d851e41bce 100644 --- a/tests/components/roborock/snapshots/test_diagnostics.ambr +++ b/tests/components/roborock/snapshots/test_diagnostics.ambr @@ -260,7 +260,17 @@ 'dockType': 3, 'dustCollectionStatus': 0, 'errorCode': 0, + 'errorCodeName': 'none', 'fanPower': 102, + 'fanPowerName': 'balanced', + 'fanPowerOptions': list([ + 'off', + 'quiet', + 'balanced', + 'turbo', + 'max', + 'custom', + ]), 'homeSecEnablePassword': 0, 'homeSecStatus': 0, 'inCleaning': 0, @@ -274,10 +284,12 @@ 'mapStatus': 3, 'mopForbiddenEnable': 1, 'mopMode': 300, + 'mopModeName': 'standard', 'msgSeq': 458, 'msgVer': 2, 'squareMeterCleanArea': 21.0, 'state': 8, + 'stateName': 'charging', 'switchMapMode': 0, 'unsaveMapFlag': 0, 'unsaveMapReason': 0, @@ -285,6 +297,7 @@ 'washReady': 0, 'waterBoxCarriageStatus': 1, 'waterBoxMode': 203, + 'waterBoxModeName': 'intense', 'waterBoxStatus': 1, 'waterShortageStatus': 0, }), @@ -521,7 +534,17 @@ 'dockType': 3, 'dustCollectionStatus': 0, 'errorCode': 0, + 'errorCodeName': 'none', 'fanPower': 102, + 'fanPowerName': 'balanced', + 'fanPowerOptions': list([ + 'off', + 'quiet', + 'balanced', + 'turbo', + 'max', + 'custom', + ]), 'homeSecEnablePassword': 0, 'homeSecStatus': 0, 'inCleaning': 0, @@ -535,10 +558,12 @@ 'mapStatus': 3, 'mopForbiddenEnable': 1, 'mopMode': 300, + 'mopModeName': 'standard', 'msgSeq': 458, 'msgVer': 2, 'squareMeterCleanArea': 21.0, 'state': 8, + 'stateName': 'charging', 'switchMapMode': 0, 'unsaveMapFlag': 0, 'unsaveMapReason': 0, @@ -546,6 +571,7 @@ 'washReady': 0, 'waterBoxCarriageStatus': 1, 'waterBoxMode': 203, + 'waterBoxModeName': 'intense', 'waterBoxStatus': 1, 'waterShortageStatus': 0, }),