From 49388eab3ae4affc4d8a2d6915a7d46997f0cddb Mon Sep 17 00:00:00 2001 From: Luke Date: Tue, 6 Jun 2023 21:24:36 -0400 Subject: [PATCH] Add diagnostics to Roborock (#94099) * Add diagnostics * Update homeassistant/components/roborock/models.py Co-authored-by: epenet <6771947+epenet@users.noreply.github.com> * adds snapshot --------- Co-authored-by: epenet <6771947+epenet@users.noreply.github.com> --- .../components/roborock/diagnostics.py | 38 +++ homeassistant/components/roborock/models.py | 10 + tests/components/roborock/conftest.py | 5 +- tests/components/roborock/mock_data.py | 4 + .../roborock/snapshots/test_diagnostics.ambr | 303 ++++++++++++++++++ tests/components/roborock/test_diagnostics.py | 23 ++ 6 files changed, 381 insertions(+), 2 deletions(-) create mode 100644 homeassistant/components/roborock/diagnostics.py create mode 100644 tests/components/roborock/snapshots/test_diagnostics.ambr create mode 100644 tests/components/roborock/test_diagnostics.py diff --git a/homeassistant/components/roborock/diagnostics.py b/homeassistant/components/roborock/diagnostics.py new file mode 100644 index 00000000000..e5fcc834267 --- /dev/null +++ b/homeassistant/components/roborock/diagnostics.py @@ -0,0 +1,38 @@ +"""Support for the Airzone diagnostics.""" +from __future__ import annotations + +from typing import Any + +from homeassistant.components.diagnostics.util import async_redact_data +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_UNIQUE_ID +from homeassistant.core import HomeAssistant + +from .const import DOMAIN +from .coordinator import RoborockDataUpdateCoordinator + +TO_REDACT_CONFIG = ["token", "sn", "rruid", CONF_UNIQUE_ID, "username", "uid"] + +TO_REDACT_COORD = ["duid", "localKey", "mac", "bssid"] + + +async def async_get_config_entry_diagnostics( + hass: HomeAssistant, config_entry: ConfigEntry +) -> dict[str, Any]: + """Return diagnostics for a config entry.""" + coordinators: dict[str, RoborockDataUpdateCoordinator] = hass.data[DOMAIN][ + config_entry.entry_id + ] + + return { + "config_entry": async_redact_data(config_entry.data, TO_REDACT_CONFIG), + "coordinators": { + f"**REDACTED-{i}**": { + "roborock_device_info": async_redact_data( + coordinator.roborock_device_info.as_dict(), TO_REDACT_COORD + ), + "api": coordinator.api.diagnostic_data, + } + for i, coordinator in enumerate(coordinators.values()) + }, + } diff --git a/homeassistant/components/roborock/models.py b/homeassistant/components/roborock/models.py index a30c84ce1da..c1d32df2d6d 100644 --- a/homeassistant/components/roborock/models.py +++ b/homeassistant/components/roborock/models.py @@ -1,5 +1,6 @@ """Roborock Models.""" from dataclasses import dataclass +from typing import Any from roborock.containers import HomeDataDevice, HomeDataProduct, NetworkInfo from roborock.roborock_typing import DeviceProp @@ -13,3 +14,12 @@ class RoborockHassDeviceInfo: network_info: NetworkInfo product: HomeDataProduct props: DeviceProp + + def as_dict(self) -> dict[str, dict[str, Any]]: + """Turn RoborockHassDeviceInfo into a dictionary.""" + return { + "device": self.device.as_dict(), + "network_info": self.network_info.as_dict(), + "product": self.product.as_dict(), + "props": self.props.as_dict(), + } diff --git a/tests/components/roborock/conftest.py b/tests/components/roborock/conftest.py index b311f84f94a..d9c11bead74 100644 --- a/tests/components/roborock/conftest.py +++ b/tests/components/roborock/conftest.py @@ -12,7 +12,7 @@ from homeassistant.const import CONF_USERNAME from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component -from .mock_data import BASE_URL, HOME_DATA, PROP, USER_DATA, USER_EMAIL +from .mock_data import BASE_URL, HOME_DATA, NETWORK_INFO, PROP, USER_DATA, USER_EMAIL from tests.common import MockConfigEntry @@ -54,7 +54,8 @@ async def setup_entry( "homeassistant.components.roborock.RoborockApiClient.get_home_data", return_value=HOME_DATA, ), patch( - "homeassistant.components.roborock.RoborockMqttClient.get_networking" + "homeassistant.components.roborock.RoborockMqttClient.get_networking", + return_value=NETWORK_INFO, ), patch( "homeassistant.components.roborock.coordinator.RoborockLocalClient.get_prop", return_value=PROP, diff --git a/tests/components/roborock/mock_data.py b/tests/components/roborock/mock_data.py index 15e69cee9d9..8155c10fdbd 100644 --- a/tests/components/roborock/mock_data.py +++ b/tests/components/roborock/mock_data.py @@ -1,6 +1,8 @@ """Mock data for Roborock tests.""" from __future__ import annotations +import datetime + from roborock.containers import ( CleanRecord, CleanSummary, @@ -320,6 +322,8 @@ DND_TIMER = DnDTimer.from_dict( "enabled": 1, } ) +DND_TIMER.start_time = datetime.datetime(year=2023, month=6, day=1, hour=22) +DND_TIMER.end_time = datetime.datetime(year=2023, month=6, day=2, hour=7) STATUS = S7Status.from_dict( { diff --git a/tests/components/roborock/snapshots/test_diagnostics.ambr b/tests/components/roborock/snapshots/test_diagnostics.ambr new file mode 100644 index 00000000000..5cb9b109368 --- /dev/null +++ b/tests/components/roborock/snapshots/test_diagnostics.ambr @@ -0,0 +1,303 @@ +# serializer version: 1 +# name: test_diagnostics + dict({ + 'config_entry': dict({ + 'base_url': 'https://usiot.roborock.com', + 'user_data': dict({ + 'avatarurl': 'https://files.roborock.com/iottest/default_avatar.png', + 'country': 'US', + 'countrycode': '1', + 'nickname': 'user_nickname', + 'region': 'us', + 'rriot': dict({ + 'h': 'abc123', + 'k': 'abc123', + 'r': dict({ + 'a': 'https://api-us.roborock.com', + 'l': 'https://wood-us.roborock.com', + 'm': 'ssl://mqtt-us-2.roborock.com:8883', + 'r': 'US', + }), + 's': 'abc123', + 'u': 'abc123', + }), + 'rruid': '**REDACTED**', + 'token': '**REDACTED**', + 'tokentype': '', + 'tuyaDeviceState': 2, + 'uid': '**REDACTED**', + }), + 'username': '**REDACTED**', + }), + 'coordinators': dict({ + '**REDACTED-0**': dict({ + 'api': dict({ + }), + 'roborock_device_info': dict({ + 'device': dict({ + 'activeTime': 1672364449, + 'deviceStatus': dict({ + '120': 0, + '121': 8, + '122': 100, + '123': 102, + '124': 203, + '125': 94, + '126': 90, + '127': 87, + '128': 0, + '133': 1, + }), + 'duid': '**REDACTED**', + 'extra': '{"RRPhotoPrivacyVersion": "1"}', + 'featureSet': '2234201184108543', + 'fv': '02.56.02', + 'iconUrl': '', + 'localKey': '**REDACTED**', + 'name': 'Roborock S7 MaxV', + 'newFeatureSet': '0000000000002041', + 'online': True, + 'productId': 'abc123', + 'pv': '1.0', + 'roomId': 2362003, + 'share': False, + 'silentOtaSwitch': True, + 'sn': 'abc123', + 'timeZoneId': 'America/Los_Angeles', + 'tuyaMigrated': False, + }), + 'network_info': dict({ + 'bssid': '**REDACTED**', + 'ip': '123.232.12.1', + 'mac': '**REDACTED**', + 'rssi': 90, + 'ssid': 'wifi', + }), + 'product': dict({ + 'capability': 0, + 'category': 'robot.vacuum.cleaner', + 'code': 'a27', + 'id': 'abc123', + 'model': 'roborock.vacuum.a27', + 'name': 'Roborock S7 MaxV', + 'schema': list([ + dict({ + 'code': 'rpc_request', + 'id': '101', + 'mode': 'rw', + 'name': 'rpc_request', + 'type': 'RAW', + }), + dict({ + 'code': 'rpc_response', + 'id': '102', + 'mode': 'rw', + 'name': 'rpc_response', + 'type': 'RAW', + }), + dict({ + 'code': 'error_code', + 'id': '120', + 'mode': 'ro', + 'name': '错误代码', + 'type': 'ENUM', + }), + dict({ + 'code': 'state', + 'id': '121', + 'mode': 'ro', + 'name': '设备状态', + 'type': 'ENUM', + }), + dict({ + 'code': 'battery', + 'id': '122', + 'mode': 'ro', + 'name': '设备电量', + 'type': 'ENUM', + }), + dict({ + 'code': 'fan_power', + 'id': '123', + 'mode': 'rw', + 'name': '清扫模式', + 'type': 'ENUM', + }), + dict({ + 'code': 'water_box_mode', + 'id': '124', + 'mode': 'rw', + 'name': '拖地模式', + 'type': 'ENUM', + }), + dict({ + 'code': 'main_brush_life', + 'id': '125', + 'mode': 'rw', + 'name': '主刷寿命', + 'type': 'VALUE', + }), + dict({ + 'code': 'side_brush_life', + 'id': '126', + 'mode': 'rw', + 'name': '边刷寿命', + 'type': 'VALUE', + }), + dict({ + 'code': 'filter_life', + 'id': '127', + 'mode': 'rw', + 'name': '滤网寿命', + 'type': 'VALUE', + }), + dict({ + 'code': 'additional_props', + 'id': '128', + 'mode': 'ro', + 'name': '额外状态', + 'type': 'RAW', + }), + dict({ + 'code': 'task_complete', + 'id': '130', + 'mode': 'ro', + 'name': '完成事件', + 'type': 'RAW', + }), + dict({ + 'code': 'task_cancel_low_power', + 'id': '131', + 'mode': 'ro', + 'name': '电量不足任务取消', + 'type': 'RAW', + }), + dict({ + 'code': 'task_cancel_in_motion', + 'id': '132', + 'mode': 'ro', + 'name': '运动中任务取消', + 'type': 'RAW', + }), + dict({ + 'code': 'charge_status', + 'id': '133', + 'mode': 'ro', + 'name': '充电状态', + 'type': 'RAW', + }), + dict({ + 'code': 'drying_status', + 'id': '134', + 'mode': 'ro', + 'name': '烘干状态', + 'type': 'RAW', + }), + ]), + }), + 'props': dict({ + 'cleanSummary': dict({ + 'cleanArea': 1159182500, + 'cleanCount': 31, + 'cleanTime': 74382, + 'dustCollectionCount': 25, + 'records': list([ + 1672543330, + 1672458041, + ]), + 'squareMeterCleanArea': 1159.2, + }), + 'consumable': dict({ + 'cleaningBrushWorkTimes': 65, + 'dustCollectionWorkTimes': 25, + 'filterElementWorkTime': 0, + 'filterTimeLeft': 465618, + 'filterWorkTime': 74382, + 'mainBrushTimeLeft': 1005618, + 'mainBrushWorkTime': 74382, + 'sensorDirtyTime': 74382, + 'sensorTimeLeft': 33618, + 'sideBrushTimeLeft': 645618, + 'sideBrushWorkTime': 74382, + 'strainerWorkTimes': 65, + }), + 'dndTimer': dict({ + 'enabled': 1, + 'endHour': 7, + 'endMinute': 0, + 'endTime': '2023-06-02T07:00:00', + 'startHour': 22, + 'startMinute': 0, + 'startTime': '2023-06-01T22:00:00', + }), + 'lastCleanRecord': dict({ + 'area': 20965000, + 'avoidCount': 19, + 'begin': 1672543330, + 'cleanType': 3, + 'complete': 1, + 'duration': 1176, + 'dustCollectionStatus': 1, + 'end': 1672544638, + 'error': 0, + 'finishReason': 56, + 'mapFlag': 0, + 'squareMeterArea': 21.0, + 'startType': 2, + 'washCount': 2, + }), + 'status': dict({ + 'adbumperStatus': list([ + 0, + 0, + 0, + ]), + 'autoDustCollection': 1, + 'avoidCount': 19, + 'backType': -1, + 'battery': 100, + 'cameraStatus': 3457, + 'chargeStatus': 1, + 'cleanArea': 20965000, + 'cleanTime': 1176, + 'collisionAvoidStatus': 1, + 'debugMode': 0, + 'dndEnabled': 0, + 'dockErrorStatus': 0, + 'dockType': 3, + 'dustCollectionStatus': 0, + 'errorCode': 0, + 'fanPower': 102, + 'homeSecEnablePassword': 0, + 'homeSecStatus': 0, + 'inCleaning': 0, + 'inFreshState': 1, + 'inReturning': 0, + 'isExploring': 0, + 'isLocating': 0, + 'labStatus': 1, + 'lockStatus': 0, + 'mapPresent': 1, + 'mapStatus': 3, + 'mopForbiddenEnable': 1, + 'mopMode': 300, + 'msgSeq': 458, + 'msgVer': 2, + 'squareMeterCleanArea': 21.0, + 'state': 8, + 'switchMapMode': 0, + 'unsaveMapFlag': 0, + 'unsaveMapReason': 0, + 'washPhase': 0, + 'washReady': 0, + 'waterBoxCarriageStatus': 1, + 'waterBoxMode': 203, + 'waterBoxStatus': 1, + 'waterShortageStatus': 0, + }), + }), + }), + }), + }), + }) +# --- diff --git a/tests/components/roborock/test_diagnostics.py b/tests/components/roborock/test_diagnostics.py new file mode 100644 index 00000000000..a10cbcf057e --- /dev/null +++ b/tests/components/roborock/test_diagnostics.py @@ -0,0 +1,23 @@ +"""Tests for the diagnostics data provided by the Roborock integration.""" + +from syrupy.assertion import SnapshotAssertion + +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry +from tests.components.diagnostics import get_diagnostics_for_config_entry +from tests.typing import ClientSessionGenerator + + +async def test_diagnostics( + hass: HomeAssistant, + hass_client: ClientSessionGenerator, + bypass_api_fixture, + setup_entry: MockConfigEntry, + snapshot: SnapshotAssertion, +) -> None: + """Test diagnostics for config entry.""" + result = await get_diagnostics_for_config_entry(hass, hass_client, setup_entry) + + assert isinstance(result, dict) + assert result == snapshot