diff --git a/homeassistant/components/roborock/__init__.py b/homeassistant/components/roborock/__init__.py index 50ffbaaa6e1..3743faa32d8 100644 --- a/homeassistant/components/roborock/__init__.py +++ b/homeassistant/components/roborock/__init__.py @@ -10,7 +10,6 @@ import logging from typing import Any from roborock import HomeDataRoom, RoborockException, RoborockInvalidCredentials -from roborock.code_mappings import RoborockCategory from roborock.containers import DeviceData, HomeDataDevice, HomeDataProduct, UserData from roborock.version_1_apis.roborock_mqtt_client_v1 import RoborockMqttClientV1 from roborock.version_a01_apis import RoborockMqttClientA01 @@ -151,8 +150,7 @@ async def setup_device( hass, user_data, device, product_info, home_data_rooms ) if device.pv == "A01": - if product_info.category == RoborockCategory.WET_DRY_VAC: - return await setup_device_a01(hass, user_data, device, product_info) + return await setup_device_a01(hass, user_data, device, product_info) _LOGGER.info( "Not adding device %s because its protocol version %s or category %s is not supported", device.duid, diff --git a/homeassistant/components/roborock/coordinator.py b/homeassistant/components/roborock/coordinator.py index a0e441201bb..615d18c3019 100644 --- a/homeassistant/components/roborock/coordinator.py +++ b/homeassistant/components/roborock/coordinator.py @@ -8,6 +8,7 @@ from functools import cached_property import logging from roborock import HomeDataRoom +from roborock.code_mappings import RoborockCategory from roborock.containers import DeviceData, HomeDataDevice, HomeDataProduct, NetworkInfo from roborock.exceptions import RoborockException from roborock.roborock_message import RoborockDyadDataProtocol, RoborockZeoProtocol @@ -179,14 +180,27 @@ class RoborockDataUpdateCoordinatorA01( model=product_info.model, sw_version=device.fv, ) - self.request_protocols: list[RoborockDyadDataProtocol | RoborockZeoProtocol] = [ - RoborockDyadDataProtocol.STATUS, - RoborockDyadDataProtocol.POWER, - RoborockDyadDataProtocol.MESH_LEFT, - RoborockDyadDataProtocol.BRUSH_LEFT, - RoborockDyadDataProtocol.ERROR, - RoborockDyadDataProtocol.TOTAL_RUN_TIME, - ] + self.request_protocols: list[ + RoborockDyadDataProtocol | RoborockZeoProtocol + ] = [] + if product_info.category == RoborockCategory.WET_DRY_VAC: + self.request_protocols = [ + RoborockDyadDataProtocol.STATUS, + RoborockDyadDataProtocol.POWER, + RoborockDyadDataProtocol.MESH_LEFT, + RoborockDyadDataProtocol.BRUSH_LEFT, + RoborockDyadDataProtocol.ERROR, + RoborockDyadDataProtocol.TOTAL_RUN_TIME, + ] + elif product_info.category == RoborockCategory.WASHING_MACHINE: + self.request_protocols = [ + RoborockZeoProtocol.STATE, + RoborockZeoProtocol.COUNTDOWN, + RoborockZeoProtocol.WASHING_LEFT, + RoborockZeoProtocol.ERROR, + ] + else: + _LOGGER.warning("The device you added is not yet supported") self.roborock_device_info = RoborockA01HassDeviceInfo(device, product_info) async def _async_update_data( diff --git a/homeassistant/components/roborock/icons.json b/homeassistant/components/roborock/icons.json index babde739775..6a615ab82a1 100644 --- a/homeassistant/components/roborock/icons.json +++ b/homeassistant/components/roborock/icons.json @@ -75,6 +75,18 @@ }, "dock_error": { "default": "mdi:garage-open" + }, + "zeo_error": { + "default": "mdi:alert-circle" + }, + "zeo_state": { + "default": "mdi:information-outline" + }, + "washing_left": { + "default": "mdi:clock-outline" + }, + "countdown": { + "default": "mdi:clock-outline" } }, "switch": { diff --git a/homeassistant/components/roborock/sensor.py b/homeassistant/components/roborock/sensor.py index 36ee5fb02ce..b247dc6936d 100644 --- a/homeassistant/components/roborock/sensor.py +++ b/homeassistant/components/roborock/sensor.py @@ -6,14 +6,18 @@ from collections.abc import Callable from dataclasses import dataclass import datetime -from roborock.code_mappings import DyadError, RoborockDyadStateCode +from roborock.code_mappings import DyadError, RoborockDyadStateCode, ZeoError, ZeoState from roborock.containers import ( RoborockDockErrorCode, RoborockDockTypeCode, RoborockErrorCode, RoborockStateCode, ) -from roborock.roborock_message import RoborockDataProtocol, RoborockDyadDataProtocol +from roborock.roborock_message import ( + RoborockDataProtocol, + RoborockDyadDataProtocol, + RoborockZeoProtocol, +) from roborock.roborock_typing import DeviceProp from homeassistant.components.sensor import ( @@ -49,7 +53,7 @@ class RoborockSensorDescription(SensorEntityDescription): class RoborockSensorDescriptionA01(SensorEntityDescription): """A class that describes Roborock sensors.""" - data_protocol: RoborockDyadDataProtocol + data_protocol: RoborockDyadDataProtocol | RoborockZeoProtocol def _dock_error_value_fn(properties: DeviceProp) -> str | None: @@ -247,6 +251,38 @@ A01_SENSOR_DESCRIPTIONS: list[RoborockSensorDescriptionA01] = [ translation_key="total_cleaning_time", entity_category=EntityCategory.DIAGNOSTIC, ), + RoborockSensorDescriptionA01( + key="state", + data_protocol=RoborockZeoProtocol.STATE, + translation_key="zeo_state", + entity_category=EntityCategory.DIAGNOSTIC, + device_class=SensorDeviceClass.ENUM, + options=ZeoState.keys(), + ), + RoborockSensorDescriptionA01( + key="countdown", + native_unit_of_measurement=UnitOfTime.MINUTES, + data_protocol=RoborockZeoProtocol.COUNTDOWN, + device_class=SensorDeviceClass.DURATION, + translation_key="countdown", + entity_category=EntityCategory.DIAGNOSTIC, + ), + RoborockSensorDescriptionA01( + key="washing_left", + native_unit_of_measurement=UnitOfTime.MINUTES, + data_protocol=RoborockZeoProtocol.WASHING_LEFT, + device_class=SensorDeviceClass.DURATION, + translation_key="washing_left", + entity_category=EntityCategory.DIAGNOSTIC, + ), + RoborockSensorDescriptionA01( + key="error", + data_protocol=RoborockZeoProtocol.ERROR, + device_class=SensorDeviceClass.ENUM, + translation_key="zeo_error", + entity_category=EntityCategory.DIAGNOSTIC, + options=ZeoError.keys(), + ), ] diff --git a/homeassistant/components/roborock/strings.json b/homeassistant/components/roborock/strings.json index 362e0e4aff8..03ac9f5362e 100644 --- a/homeassistant/components/roborock/strings.json +++ b/homeassistant/components/roborock/strings.json @@ -152,6 +152,9 @@ "clean_percent": { "name": "Cleaning progress" }, + "countdown": { + "name": "Countdown" + }, "dock_error": { "name": "Dock error", "state": { @@ -272,6 +275,47 @@ "mopping_roller_2": "[%key:component::roborock::entity::sensor::vacuum_error::state::mopping_roller_1%]", "temperature_protection": "Unit temperature protection" } + }, + "washing_left": { + "name": "Washing left" + }, + "zeo_error": { + "name": "Error", + "state": { + "none": "[%key:component::roborock::entity::sensor::vacuum_error::state::none%]", + "refill_error": "Refill error", + "drain_error": "Drain error", + "door_lock_error": "Door lock error", + "water_level_error": "Water level error", + "inverter_error": "Inverter error", + "heating_error": "Heating error", + "temperature_error": "Temperature error", + "communication_error": "Communication error", + "drying_error": "Drying error", + "drying_error_e_12": "Drying error E12", + "drying_error_e_13": "Drying error E13", + "drying_error_e_14": "Drying error E14", + "drying_error_e_15": "Drying error E15", + "drying_error_e_16": "Drying error E16", + "drying_error_water_flow": "Check water flow", + "drying_error_restart": "Restart the washer", + "spin_error": "Re-arrange clothes" + } + }, + "zeo_state": { + "name": "State", + "state": { + "standby": "Standby", + "weighing": "Weighing", + "soaking": "Soaking", + "washing": "Washing", + "rinsing": "Rinsing", + "spinning": "Spinning", + "drying": "Drying", + "cooling": "Cooling", + "under_delay_start": "Delayed start", + "done": "Done" + } } }, "select": { diff --git a/tests/components/roborock/conftest.py b/tests/components/roborock/conftest.py index a7ebbf10af3..357c644e2fe 100644 --- a/tests/components/roborock/conftest.py +++ b/tests/components/roborock/conftest.py @@ -4,8 +4,8 @@ from copy import deepcopy from unittest.mock import patch import pytest -from roborock import RoomMapping -from roborock.code_mappings import DyadError, RoborockDyadStateCode +from roborock import RoborockCategory, RoomMapping +from roborock.code_mappings import DyadError, RoborockDyadStateCode, ZeoError, ZeoState from roborock.roborock_message import RoborockDyadDataProtocol, RoborockZeoProtocol from roborock.version_a01_apis import RoborockMqttClientA01 @@ -38,14 +38,22 @@ class A01Mock(RoborockMqttClientA01): def __init__(self, user_data, device_info, category) -> None: """Initialize the A01Mock.""" super().__init__(user_data, device_info, category) - self.protocol_responses = { - RoborockDyadDataProtocol.STATUS: RoborockDyadStateCode.drying.name, - RoborockDyadDataProtocol.POWER: 100, - RoborockDyadDataProtocol.MESH_LEFT: 111, - RoborockDyadDataProtocol.BRUSH_LEFT: 222, - RoborockDyadDataProtocol.ERROR: DyadError.none.name, - RoborockDyadDataProtocol.TOTAL_RUN_TIME: 213, - } + if category == RoborockCategory.WET_DRY_VAC: + self.protocol_responses = { + RoborockDyadDataProtocol.STATUS: RoborockDyadStateCode.drying.name, + RoborockDyadDataProtocol.POWER: 100, + RoborockDyadDataProtocol.MESH_LEFT: 111, + RoborockDyadDataProtocol.BRUSH_LEFT: 222, + RoborockDyadDataProtocol.ERROR: DyadError.none.name, + RoborockDyadDataProtocol.TOTAL_RUN_TIME: 213, + } + elif category == RoborockCategory.WASHING_MACHINE: + self.protocol_responses: list[RoborockZeoProtocol] = { + RoborockZeoProtocol.STATE: ZeoState.drying.name, + RoborockZeoProtocol.COUNTDOWN: 0, + RoborockZeoProtocol.WASHING_LEFT: 253, + RoborockZeoProtocol.ERROR: ZeoError.none.name, + } async def update_values( self, dyad_data_protocols: list[RoborockDyadDataProtocol | RoborockZeoProtocol] diff --git a/tests/components/roborock/snapshots/test_diagnostics.ambr b/tests/components/roborock/snapshots/test_diagnostics.ambr index 4318b537a2c..805a498041a 100644 --- a/tests/components/roborock/snapshots/test_diagnostics.ambr +++ b/tests/components/roborock/snapshots/test_diagnostics.ambr @@ -951,6 +951,355 @@ }), }), }), + '**REDACTED-3**': dict({ + 'api': dict({ + 'misc_info': dict({ + }), + }), + 'roborock_device_info': dict({ + 'device': dict({ + 'activeTime': 1699964128, + 'deviceStatus': dict({ + '10001': '{"f":"t"}', + '10005': '{"sn":"zeo_sn","ssid":"internet","timezone":"Europe/Berlin","posix_timezone":"CET-1CEST,M3.5.0,M10.5.0/3","ip":"192.111.11.11","mac":"b0:4a:00:00:00:00","rssi":-57,"oba":{"language":"en","name":"A.03.0403_CE","bom":"A.03.0403","location":"de","wifiplan":"EU","timezone":"CET-1CEST,M3.5.0,M10.5.0/3;Europe/Berlin","logserver":"awsde0","loglevel":"4","featureset":"0"}}', + '10007': '{"mqttOtaData":{"mqttOtaStatus":{"status":"IDLE"}}}', + '200': 1, + '201': 0, + '202': 1, + '203': 7, + '204': 1, + '205': 33, + '206': 0, + '207': 4, + '208': 2, + '209': 7, + '210': 1, + '211': 1, + '212': 1, + '213': 2, + '214': 2, + '217': 0, + '218': 227, + '219': 0, + '220': 0, + '221': 0, + '222': 347414, + '223': 0, + '224': 21, + '225': 0, + '226': 0, + '227': 1, + '232': 0, + }), + 'duid': '**REDACTED**', + 'f': False, + 'featureSet': '0', + 'fv': '01.00.94', + 'iconUrl': '', + 'localKey': '**REDACTED**', + 'name': 'Zeo One', + 'newFeatureSet': '40', + 'online': True, + 'productId': 'zeo_id', + 'pv': 'A01', + 'share': True, + 'shareTime': 1712763572, + 'silentOtaSwitch': False, + 'sn': 'zeo_sn', + 'timeZoneId': 'Europe/Berlin', + 'tuyaMigrated': False, + }), + 'product': dict({ + 'capability': 2, + 'category': 'roborock.wm', + 'id': 'zeo_id', + 'model': 'roborock.wm.a102', + 'name': 'Zeo One', + 'schema': list([ + dict({ + 'code': 'drying_status', + 'id': '134', + 'mode': 'ro', + 'name': '烘干状态', + 'type': 'RAW', + }), + dict({ + 'code': 'start', + 'id': '200', + 'mode': 'rw', + 'name': '启动', + 'type': 'BOOL', + }), + dict({ + 'code': 'pause', + 'id': '201', + 'mode': 'rw', + 'name': '暂停', + 'type': 'BOOL', + }), + dict({ + 'code': 'shutdown', + 'id': '202', + 'mode': 'rw', + 'name': '关机', + 'type': 'BOOL', + }), + dict({ + 'code': 'status', + 'id': '203', + 'mode': 'ro', + 'name': '状态', + 'type': 'VALUE', + }), + dict({ + 'code': 'mode', + 'id': '204', + 'mode': 'rw', + 'name': '模式', + 'type': 'VALUE', + }), + dict({ + 'code': 'program', + 'id': '205', + 'mode': 'rw', + 'name': '程序', + 'type': 'VALUE', + }), + dict({ + 'code': 'child_lock', + 'id': '206', + 'mode': 'rw', + 'name': '童锁', + 'type': 'BOOL', + }), + dict({ + 'code': 'temp', + 'id': '207', + 'mode': 'rw', + 'name': '洗涤温度', + 'type': 'VALUE', + }), + dict({ + 'code': 'rinse_times', + 'id': '208', + 'mode': 'rw', + 'name': '漂洗次数', + 'type': 'VALUE', + }), + dict({ + 'code': 'spin_level', + 'id': '209', + 'mode': 'rw', + 'name': '滚筒转速', + 'type': 'VALUE', + }), + dict({ + 'code': 'drying_mode', + 'id': '210', + 'mode': 'rw', + 'name': '干燥度', + 'type': 'VALUE', + }), + dict({ + 'code': 'detergent_set', + 'id': '211', + 'mode': 'rw', + 'name': '自动投放-洗衣液', + 'type': 'BOOL', + }), + dict({ + 'code': 'softener_set', + 'id': '212', + 'mode': 'rw', + 'name': '自动投放-柔顺剂', + 'type': 'BOOL', + }), + dict({ + 'code': 'detergent_type', + 'id': '213', + 'mode': 'rw', + 'name': '洗衣液投放量', + 'type': 'VALUE', + }), + dict({ + 'code': 'softener_type', + 'id': '214', + 'mode': 'rw', + 'name': '柔顺剂投放量', + 'type': 'VALUE', + }), + dict({ + 'code': 'countdown', + 'id': '217', + 'mode': 'rw', + 'name': '预约时间', + 'type': 'VALUE', + }), + dict({ + 'code': 'washing_left', + 'id': '218', + 'mode': 'ro', + 'name': '洗衣剩余时间', + 'type': 'VALUE', + }), + dict({ + 'code': 'doorlock_state', + 'id': '219', + 'mode': 'ro', + 'name': '门锁状态', + 'type': 'BOOL', + }), + dict({ + 'code': 'error', + 'id': '220', + 'mode': 'ro', + 'name': '故障', + 'type': 'VALUE', + }), + dict({ + 'code': 'custom_param_save', + 'id': '221', + 'mode': 'rw', + 'name': '云程序设置', + 'type': 'VALUE', + }), + dict({ + 'code': 'custom_param_get', + 'id': '222', + 'mode': 'ro', + 'name': '云程序读取', + 'type': 'VALUE', + }), + dict({ + 'code': 'sound_set', + 'id': '223', + 'mode': 'rw', + 'name': '提示音', + 'type': 'BOOL', + }), + dict({ + 'code': 'times_after_clean', + 'id': '224', + 'mode': 'ro', + 'name': '距离上次筒自洁次数', + 'type': 'VALUE', + }), + dict({ + 'code': 'default_setting', + 'id': '225', + 'mode': 'rw', + 'name': '记忆洗衣偏好开关', + 'type': 'BOOL', + }), + dict({ + 'code': 'detergent_empty', + 'id': '226', + 'mode': 'ro', + 'name': '洗衣液用尽', + 'type': 'BOOL', + }), + dict({ + 'code': 'softener_empty', + 'id': '227', + 'mode': 'ro', + 'name': '柔顺剂用尽', + 'type': 'BOOL', + }), + dict({ + 'code': 'light_setting', + 'id': '229', + 'mode': 'rw', + 'name': '筒灯设定', + 'type': 'BOOL', + }), + dict({ + 'code': 'detergent_volume', + 'id': '230', + 'mode': 'rw', + 'name': '洗衣液投放量(单次)', + 'type': 'VALUE', + }), + dict({ + 'code': 'softener_volume', + 'id': '231', + 'mode': 'rw', + 'name': '柔顺剂投放量(单次)', + 'type': 'VALUE', + }), + dict({ + 'code': 'app_authorization', + 'id': '232', + 'mode': 'rw', + 'name': '远程控制授权', + 'type': 'VALUE', + }), + dict({ + 'code': 'id_query', + 'id': '10000', + 'mode': 'rw', + 'name': 'ID点查询', + 'type': 'STRING', + }), + dict({ + 'code': 'f_c', + 'id': '10001', + 'mode': 'ro', + 'name': '防串货', + 'type': 'STRING', + }), + dict({ + 'code': 'snd_state', + 'id': '10004', + 'mode': 'rw', + 'name': '语音包/OBA信息', + 'type': 'STRING', + }), + dict({ + 'code': 'product_info', + 'id': '10005', + 'mode': 'ro', + 'name': '产品信息', + 'type': 'STRING', + }), + dict({ + 'code': 'privacy_info', + 'id': '10006', + 'mode': 'rw', + 'name': '隐私协议', + 'type': 'STRING', + }), + dict({ + 'code': 'ota_nfo', + 'id': '10007', + 'mode': 'rw', + 'name': 'OTA info', + 'type': 'STRING', + }), + dict({ + 'code': 'washing_log', + 'id': '10008', + 'mode': 'ro', + 'name': '洗衣记录', + 'type': 'BOOL', + }), + dict({ + 'code': 'rpc_req', + 'id': '10101', + 'mode': 'wo', + 'name': 'rpc req', + 'type': 'STRING', + }), + dict({ + 'code': 'rpc_resp', + 'id': '10102', + 'mode': 'ro', + 'name': 'rpc resp', + 'type': 'STRING', + }), + ]), + }), + }), + }), }), }) # --- diff --git a/tests/components/roborock/test_init.py b/tests/components/roborock/test_init.py index 704f093d3fd..cace9a8ed67 100644 --- a/tests/components/roborock/test_init.py +++ b/tests/components/roborock/test_init.py @@ -176,3 +176,21 @@ async def test_not_supported_protocol( await async_setup_component(hass, DOMAIN, {}) await hass.async_block_till_done() assert "because its protocol version random" in caplog.text + + +async def test_not_supported_a01_device( + hass: HomeAssistant, + bypass_api_fixture, + mock_roborock_entry: MockConfigEntry, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test that we output a message on incorrect category.""" + home_data_copy = deepcopy(HOME_DATA) + home_data_copy.products[2].category = "random" + with patch( + "homeassistant.components.roborock.RoborockApiClient.get_home_data_v2", + return_value=home_data_copy, + ): + await async_setup_component(hass, DOMAIN, {}) + await hass.async_block_till_done() + assert "The device you added is not yet supported" in caplog.text diff --git a/tests/components/roborock/test_sensor.py b/tests/components/roborock/test_sensor.py index e608895ca43..908754f3b92 100644 --- a/tests/components/roborock/test_sensor.py +++ b/tests/components/roborock/test_sensor.py @@ -21,7 +21,7 @@ from tests.common import MockConfigEntry async def test_sensors(hass: HomeAssistant, setup_entry: MockConfigEntry) -> None: """Test sensors and check test values are correctly set.""" - assert len(hass.states.async_all("sensor")) == 34 + assert len(hass.states.async_all("sensor")) == 38 assert hass.states.get("sensor.roborock_s7_maxv_main_brush_time_left").state == str( MAIN_BRUSH_REPLACE_TIME - 74382 ) @@ -60,6 +60,10 @@ async def test_sensors(hass: HomeAssistant, setup_entry: MockConfigEntry) -> Non assert hass.states.get("sensor.dyad_pro_roller_left").state == "222" assert hass.states.get("sensor.dyad_pro_error").state == "none" assert hass.states.get("sensor.dyad_pro_total_cleaning_time").state == "213" + assert hass.states.get("sensor.zeo_one_state").state == "drying" + assert hass.states.get("sensor.zeo_one_countdown").state == "0" + assert hass.states.get("sensor.zeo_one_washing_left").state == "253" + assert hass.states.get("sensor.zeo_one_error").state == "none" async def test_listener_update(